Passing Data to Screen from FlatList Component - react-native

I created a custom component called AchievementBlock which is as follows:
import React from 'react';
import { View, StyleSheet, Image, TouchableOpacity } from 'react-native';
const AchievementBlock = ({ onPress }) => {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.AchievmentBlockStyle}>
<Image source={require('../../assets/images/Placeholder_Achievement.png')} />
</View>
</TouchableOpacity>
)
}
And used that component as a FlatList in my UserProfileScreen with a the following data:
const tournaments = [
{ title: 'Tournament Title #1', placement: '2nd', desc: 'Description Here', logo: require('../../assets/images/TournamentLogo.png') },
{ title: 'Tournament Title #2', placement: '1st', desc: 'Description Here', logo: require('../../assets/images/TournamentLogo.png') },
{ title: 'Tournament Title #3', placement: '3rd', desc: 'Description Here', logo: require('../../assets/images/TournamentLogo.png') },
]
I then return as follows:
return (
<View style={styles.container}>
<FlatList
data={tournaments}
numColumns={3}
keyExtractor={(tournaments) => {tournaments.title}}
renderItem={({ item }) => {
return (
<AchievementBlock
title={item.title}
placement={item.placement}
onPress={() => navigation.navigate('Achievement')}/>
)
}}
/>
</View>
)
Now I'm stuck on pulling through each rendered FlatList item's data to a new screen when clicking on it.
The on press would be something like this:
onPress={() => navigation.navigate('Achievement', {item.title, item.placement, item.logo, item.desc})}
But that doesn't work and with me still being a beginner with React Native I can't seem to find the solution in pulling each items data through to that screen with the navigation.getParam() function as normal.
What am I doing wrong and how would I achieve this in the best practice?

You're on the right path as React Navigation v5 uses the function
navigation.navigate
However the way you pass the key/value of item in the params argument is incorrect.
Try deconstructing with the following
onPress={
() => navigation.navigate('Achievement', { ...item })
}
or pass in the keys explicitly if you intend to set the keys/values
onPress={
() => navigation.navigate('Achievement', {
title: item.title,
logo: item.logo,
placement: item.placement,
desc: item.desc
})
}

Passing data through navigation
You can pass data with navigation as follows
onPress={
() => navigation.navigate('Achievement', {
name: item.name,
logo: item.logo,
})
}
Access data in next screen
export const Achievement = ({route}) => {
const name = route.params.name;
const logo = route.params.logo;
}

Related

Unable to find an element with a testID

I'm building a React Native app. Within my GigsByDay component, there is a TouchableOpacity element which, when pressed, directs the user to a GigDetails screen. I'm trying to test this particular functionality using Jest and React Native Testing Library.
I've written the following test, but have received the error:
Unable to find an element with testID: gigs-today-card
The test is as follows:
describe("gigs by week component", () => {
let navigation;
beforeEach(() => {
navigation = { navigate: jest.fn() };
});
test("that when gig listing is pressed on it redirects user to Gig Details page", () => {
render(<GigsByDay navigation={navigation} />);
const gigCard = screen.getByTestId("gigs-today-card");
fireEvent.press(gigCard);
expect(navigation.navigate).toHaveBeenCalledWith("GigDetails");
});
});
The element it's testing is as follows:
<TouchableOpacity
testID="gigs-today-card"
style={styles.gigCard}
onPress={() =>
navigation.navigate('GigDetails', {
venue: item.venue,
gigName: item.gigName,
blurb: item.blurb,
isFree: item.isFree,
image: item.image,
genre: item.genre,
dateAndTime: {...item.dateAndTime},
tickets: item.tickets,
id:item.id
})
}>
<View style={styles.gigCard_items}>
<Image
style={styles.gigCard_items_img}
source={require('../assets/Icon_Gold_48x48.png')}
/>
<View>
<Text style={styles.gigCard_header}>{item.gigName}</Text>
<Text style={styles.gigCard_details}>{item.venue}</Text>
</View>
</View>
</TouchableOpacity>
I've tried fixing my test as follows, but to no success:
test("that when gig listing is pressed on it redirects user to Gig Details page", async () => {
render(<GigsByDay navigation={navigation} />);
await waitFor(() => {
expect(screen.getByTestId('gigs-today-card')).toBeTruthy()
})
const gigCard = screen.getByTestId("gigs-today-card");
fireEvent.press(gigCard);
expect(navigation.navigate).toHaveBeenCalledWith("GigDetails");
});
});
Any suggestions on how to fix this? I also tried assigning the testID to the view within the TouchableOpacity element.
For context, here's the whole GigsByDay component:
import { FC } from 'react';
import { FlatList,TouchableOpacity,StyleSheet,View,Image,Text } from 'react-native'
import { listProps } from '../routes/homeStack';
import { GigObject } from '../routes/homeStack';
type ListScreenNavigationProp = listProps['navigation']
interface Props {
gigsFromSelectedDate: GigObject[],
navigation: ListScreenNavigationProp
}
const GigsByDay:FC<Props> = ({ gigsFromSelectedDate, navigation }):JSX.Element => (
<FlatList
testID='gigs-today'
data={gigsFromSelectedDate}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TouchableOpacity
testID="gigs-today-card"
style={styles.gigCard}
onPress={() =>
navigation.navigate('GigDetails', {
venue: item.venue,
gigName: item.gigName,
blurb: item.blurb,
isFree: item.isFree,
image: item.image,
genre: item.genre,
dateAndTime: {...item.dateAndTime},
tickets: item.tickets,
id:item.id
})
}>
<View style={styles.gigCard_items}>
<Image
style={styles.gigCard_items_img}
source={require('../assets/Icon_Gold_48x48.png')}
/>
<View>
<Text style={styles.gigCard_header}>{item.gigName}</Text>
<Text style={styles.gigCard_details}>{item.venue}</Text>
</View>
</View>
</TouchableOpacity>
)}
/>
)
Please pass the gigsFromSelectedDate prop with some mock array data so that the flat list would render its elements based on the array length. Currently, you are not passing it. Please check the code below.
test('that when gig listing is pressed on it redirects user to Gig Details page', () => {
const mockData = [{venue: 'some venue',
gigName: 'some gigName,
blurb: 'some blurb',
isFree: 'some isFree',
image: 'some image',
genre: 'some genre',
dateAndTime: {},
tickets: ['some ticket'],
id: 'some id'}]
const screen = render(<GigsByDay navigation={navigation} gigsFromSelectedDate={mockData} />);
const gigCard = screen.getByTestId('gigs-today-card');
fireEvent.press(gigCard);
expect(navigation.navigate).toHaveBeenCalledWith('GigDetails');
});
Have you installed below package?
please check your Package.json
"#testing-library/jest-native": "^5.4.1"
if not please install it
then import it where you have written test cases.
import '#testing-library/jest-native/extend-expect';
if it is already present in then try below properties of jest-native.
const gigCard = screen.queryByTestId("gigs-today-card");
const gigCard = screen.findByTestId("gigs-today-card");

How to pass data from a FlatList to a BottomSheet in React Native?

I used BottomSheet from https://gorhom.github.io/react-native-bottom-sheet/
Say I have a FlatList that renders buttons, how can I make it that when a certain button is pressed, the BottomSheet will open and will contain whatever data I passed in it? Say, the title data.
Here's my code:
import { BottomSheetModal, BottomSheetModalProvider, BottomSheetScrollView } from '#gorhom/bottom-sheet';
import { useRef, useState, useCallback, useMemo } from "react"
const DATA = [
{
id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
title: 'John Doe',
},
{
id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
title: 'John Smith',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29d72',
title: 'John Lennon',
},
];
const Item = ({ title }) => (
<AppButton title={title} style={{ marginVertical: 10 }} />
);
const App = () => {
// ref
const bottomSheetModalRef = useRef(null);
// variables
const snapPoints = useMemo(() => ['25%', '50%'], []);
// callbacks
const handlePresentModalPress = useCallback(() => {
bottomSheetModalRef.current?.present();
}, []);
const handleSheetChanges = useCallback((index) => {
console.log('handleSheetChanges', index);
}, []);
return (
<View style={styles.container}>
<FlatList
data={DATA}
keyExtractor={item => item.id}
renderItem={({ item }) => <Item title={item.title} />}
/>
</View>
<BottomSheetModalProvider>
<BottomSheetModal
ref={bottomSheetModalRef}
index={0}
snapPoints={snapPoints}
enablePanDownToClose
onChange={handleSheetChanges}
>
<BottomSheetScrollView>
</BottomSheetScrollView>
</BottomSheetModal>
</BottomSheetModalProvider>
)
}
I only copied much of the code for the BottomSheet from the docs and I have no knowledge yet about usecallback and useref so if what I want to happen needs those two, kindly indicate it :)
If im not using a list I managed to do it but had to create many copies of bottom sheet and refs and i know that is not very efficient
Refer to the screenshot below

FlatList is re-rendering items everytime screen is scrolled

I have a list of 300 items. I'm using FlatList to render the items.
ISSUE :
List items re-rendering when I scroll the screen. Even when I have wrapped the component in React.memo.
I tried to optimise the list by tweaking the windowSize, maxToRenderPerBatch but the issue still persist.
You can check the code in below sandbox link.
Thankyou in advance !
https://codesandbox.io/s/gracious-dhawan-i4d51h?file=/src/App.js
Below is the code snippet
const data = [
{
id: 1,
first_name: "Shaina",
last_name: "Osorio",
email: "sosorio0#a8.net"
},
{
id: 2,
first_name: "Ania",
last_name: "Cotilard",
email: "acotilard1#about.me"
},
{
id: 3,
first_name: "Hagen",
last_name: "Lisciandri",
email: "hlisciandri2#nature.com"
}
]
const isEqual = (prev, next) => {
return true;
};
const RenderItem = React.memo((props) => {
const { id, first_name, email } = props;
console.log("id >>> ", id);
return (
<View
style={{
padding: 5,
backgroundColor: "lightblue",
marginVertical: 3
}}
>
<Text>First Name : {first_name}</Text>
<Text>Email : {email}</Text>
</View>
);
}, isEqual);
function App() {
return (
<View style={{ flex: 1 }}>
<FlatList
data={data}
renderItem={({ item }) => (
<RenderItem
id={item.id}
first_name={item.first_name}
email={item.email}
/>
)}
initialNumToRender={15}
maxToRenderPerBatch={15}
keyExtractor={(item) => item.id}
/>
</View>
);
}
export default App;
In your example, you are logging inside your renderItem, so when a new element comes into the rendered area, it is logged. This happens when you scroll. But this doesn't mean that the whole list will be re-rendered. Just place a conosle.log directly in the component that hosts the list, and you'll see that it's only rendered once, unlike the renderItem, which is rendered every time a new item is created by a scroll.
const App = ()=> {
console.log("Entire List Rerendered");
return (
<View style={{ flex: 1 }}>
<FlatList
data={data}
renderItem={rendering}
initialNumToRender={5}
maxToRenderPerBatch={5}
keyExtractor={(item) => item.id}
/>
</View>
);
}
Flashlist might help, because it recycles renderItems and doesn't destroy them like Flatlist does. But you should test that better.
Also check out this attempt to visually explain your console logs.
Check out FlashList by Shopify they are saying that it is much more optimized than Flatlist. maybe it can meet your needs. no harm in trying: Click Here
import React from "react";
import { View, Text, StatusBar } from "react-native";
import { FlashList } from "#shopify/flash-list";
const DATA = [
{
title: "First Item",
},
{
title: "Second Item",
},
];
const MyList = () => {
return (
<FlashList
data={DATA}
renderItem={({ item }) => <Text>{item.title}</Text>}
estimatedItemSize={200}
/>
);
};

How to maintain the scroll position when switching from vertical to horizontal orientation in Flatlist?

How can I maintain the scroll position when I switch from vertical to horizontal orientation in the flat list? A change of the horizontal prop resets the scroll position.
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => item.id}
horizontal={isHorizontal} // change triggers reset to first item
/>
Flatlist has a method called scrollToIndex that is able to scroll to the specific item. You can leverage this to return to the correct scroll position by saving the index of the item that is in view in a useState and then passing the state to the scrollToIndex function whenever ishorizontal state is changed. I've created a basic example below.
import React, { useEffect, useState, useRef } from 'react';
import { SafeAreaView, View, FlatList, StyleSheet, Text, StatusBar, Dimensions } from 'react-native';
const DATA = [
{
id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
title: 'First Item',
},
{
id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
title: 'Second Item',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29d72',
title: 'Third Item',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29d73',
title: 'forth Item',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29d74',
title: 'fifth Item',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29d71',
title: 'sixth Item',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29dfs',
title: 'seven Item',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29ddd',
title: 'eight Item',
},
];
const Item = ({ title }) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const App = () => {
const [isHorizontal, setIsHorizontal] = useState(false)
const [index, setIndex] = useState(0)
const flatListRef = useRef(null)
const renderItem = ({ item }) => (
<Item title={item.title} />
);
useEffect(() => {
const subscription = Dimensions.addEventListener(
"change",
({ window: { width, height } }) => {
if (width < height) {
setIsHorizontal(true)
} else {
setIsHorizontal(false)
}
}
);
return () => subscription?.remove();
});
useEffect(() => {
if(flatListRef.current) {
console.log(index);
flatListRef.current?.scrollToIndex({index: index})
}
}, [isHorizontal]);
const onViewRef = React.useRef((viewableItems) => {
setIndex(viewableItems.viewableItems[1].index);
});
return (
<SafeAreaView style={styles.container}>
<FlatList
initialScrollIndex={index}
ref={flatListRef}
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
onViewableItemsChanged={onViewRef.current}
isHorizontal={isHorizontal}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
margin:100
},
title: {
fontSize: 32,
},
});
export default App;

React Native: how can I implement topTabs within bottomTabs using react-native navigation

I want to create topTops within bottomTabs using react native navigation
please anyone help me
You can use react-native-tab-view for it
https://github.com/react-native-community/react-native-tab-view
import { TabView, TabBar, SceneMap } from 'react-native-tab-view';
const FirstRoute = () => (
<View style={[styles.scene, { backgroundColor: '#ff4081' }]} />
);
const SecondRoute = () => (
<View style={[styles.scene, { backgroundColor: '#673ab7' }]} />
);
export default class TabViewExample extends React.Component {
state = {
index: 0,
routes: [
{ key: 'first', title: 'First' },
{ key: 'second', title: 'Second' },
],
};
render() {
return (
<TabView
navigationState={this.state}
renderScene={SceneMap({
first: FirstRoute,
second: SecondRoute,
})}
onIndexChange={index => this.setState({ index })}
initialLayout={{ width: Dimensions.get('window').width }}
/>
);
}
}
navigationState -the current navigation state, should contain a routes array containing the list of tabs, and an index property representing the current tab
renderScene- callback which returns a React Element to use as the scene for a tab
onIndexChange-callback for when the current tab index changes, should update the navigation state
Or if you want to implement it yourself You can create top bar on the top of the screen and a container in the screen and on click on top bar item just replace the container component.