I'm working with a custom dropdown menu in React Native and am having a problem changing the text when trying to reuse the dropdown in other components.
DropdownMenu.js
export const DropdownMenu = ({dropdownMenuItems}) => {
const [isActive, setIsActive] = React.useState(false);
const onPress = () => {
setIsActive(!isActive);
};
return (
<TouchableOpacity
activeOpacity={1}
onPress={() => setIsActive(false)}
style={{flex: 1}}>
<View>
<TouchableOpacity style={styles.img} onPress={onPress}>
<Image style={styles.imgimg} source={require('./icon.png')} />
</TouchableOpacity>
<Animated.View
style={{
...styles.menu,
opacity: fadeAnim,
transform: [{translateY: translateAnim}],
}}
pointerEvents={isActive ? 'auto' : 'none'}>
<FlatList
data={dropdownMenuItems}
renderItem={({item, index}) => (
<OpenURLButton
url={item.href}
label={item.name}
style={
index === 0
? {borderTopLeftRadius: 8, borderTopRightRadius: 8}
: index === dropdownMenuItems.length - 1
? {borderBottomLeftRadius: 8, borderBottomRightRadius: 8}
: {}
}
/>
)}
/>
</Animated.View>
</View>
</TouchableOpacity>
);
};
In the CompactMenu component, I import my <DropdownMenu /> and set my initial values for my menu:
CompactMenu.js
import React from 'react';
import {SafeAreaView} from 'react-native';
import {DropdownMenu} from './DropdownMenu';
const CompactMenu = () => {
const backgroundStyle = {
backgroundColor: '#fff',
flex: 1,
display: 'flex',
};
const dropdownMenuItems = [
{name: 'Messages', href: '/messages'},
{name: 'Trips', href: '/trips'},
{name: 'Saved', href: '/saved'},
];
return (
<SafeAreaView style={backgroundStyle}>
<DropdownMenu dropdownMenuItems={dropdownMenuItems} />
</SafeAreaView>
);
};
export default CompactMenu;
After importing <CompactMenu /> into another component, I try to change the name & the href in object:
example import:
<CompactMenu dropdownMenuItems={[{name: "changed name", href: "/somePath"}]} />
However, the same strings from CompactMenu.js are displayed in the dropdown.
Being new to RN, I'm not sure of two things here.
1.) Why do the text value for "name" not change when used in a different component?
2.) Shouldn't navigation to another screen use { navigation } instead of href:? I've tried adding onPress={() => navigation.navigate('SomeScreen') in the href but I get an error.
I'm not sure what the correct solution to this is. Any help would be appreciated.
You are not using the props dropdownMenuItems that you are passing to CompactMenu. Instead, you reuse the same menu items for the DropdownMenu component everytime you create a CompactMenu.
You need to use the props that you are passing. I have kept the static items as a default value. If you would like to have these items as well and to add additional items via props, then have a look at the second solution.
Notice that I have integrated some small changes to the rest of the code as well.
const defaultItems = [
{name: 'Messages', href: '/messages'},
{name: 'Trips', href: '/trips'},
{name: 'Saved', href: '/saved'},
];
const CompactMenu = ({dropdownMenuItems = defaultItems}) => {
return (
<SafeAreaView style={styles.backgroundStyle}>
<DropdownMenu dropdownMenuItems={dropdownMenuItems} />
</SafeAreaView>
);
};
const styles = StyleSheet.create({
backgroundStyle: {
backgroundColor: '#fff',
flex: 1,
}
});
Now using the CompactMenu component can receive dropdownMenuItems and will pas them to the DropdownMenu component.
<CompactMenu dropdownMenuItems={[{name: "changed name", href: "/somePath"}]} />
If you want to keep default items and add additional items via props, we could achieve this by merging the provided props with our default items.
const defaultItems = [
{name: 'Messages', href: '/messages'},
{name: 'Trips', href: '/trips'},
{name: 'Saved', href: '/saved'},
];
const CompactMenu = ({dropdownMenuItems}) => {
return (
<SafeAreaView style={styles.backgroundStyle}>
<DropdownMenu dropdownMenuItems={[...defaultItems, ...dropdownMenuItems]} />
</SafeAreaView>
);
};
Your second questions addresses the react-navigation framework for react-native. This is a very broad topic and I am assuming from your question that you don't know how this works yet, since you have not setup the necessary structure for using it. I encourage you to go through the documentation first.
In summary, you will need to define a navigator, e.g. a stack-navigator and add a name reference for each of your screens to the dropdown menu. To keep things short, here is a minimal example on how this could work.
const MenuScreen1 = (props) {
return (...)
}
const MenuScreen2 = (props) {
return (...)
}
const CompactMenu = ({dropdownMenuItems, navigation}) => {
return (
<SafeAreaView style={styles.backgroundStyle}>
<DropdownMenu dropdownMenuItems={dropdownMenuItems} navigation={navigation} />
</SafeAreaView>
);
};
const dropdownMenuItems = [
{name: 'Menu Item 1', screen: 'Item1'},
{name: 'Menu Item 2', screen: 'Item2'},
]
const Home = ({navigation}) {
return <CompactMenu navigation={navigation} dropdownMenuItems={dropdownMenuItems} />
}
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Item1" component={MenuScreen1} />
<Stack.Screen name="Item2" component={MenuScreen2} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
Notice that I have created two screens whose names, which I have defined in the stack navigator, are provided to the CompactMenu. Notice as well that the navigation framework will pas the navigation object only to components that are defined as a screen, thus I have decided to pass the navigation object to the CompactMenu and the DropdownMenu component. You can use the useNavigation hook if you prefer this method.
Now, in your DropdownMenu you can navigate on click to the defined screens.
export const DropdownMenu = ({dropdownMenuItems, navigation}) => {
const [isActive, setIsActive] = React.useState(false);
const onPress = () => {
setIsActive(!isActive);
};
return (
<TouchableOpacity
activeOpacity={1}
onPress={() => setIsActive(false)}
style={{flex: 1}}>
<View>
<TouchableOpacity style={styles.img} onPress={onPress}>
<Image style={styles.imgimg} source={require('./icon.png')} />
</TouchableOpacity>
<Animated.View
style={{
...styles.menu,
opacity: fadeAnim,
transform: [{translateY: translateAnim}],
}}
pointerEvents={isActive ? 'auto' : 'none'}>
<FlatList
data={dropdownMenuItems}
renderItem={({item, index}) => (
<Button
onPress={() => navigation.navigate(item.screen)}
title={item.name}
style={
index === 0
? {borderTopLeftRadius: 8, borderTopRightRadius: 8}
: index === dropdownMenuItems.length - 1
? {borderBottomLeftRadius: 8, borderBottomRightRadius: 8}
: {}
}
/>
)}
/>
</Animated.View>
</View>
</TouchableOpacity>
);
};
Related
I have a tab bar that looks like this:
The two side buttons are stack navigators (Learn and Journal) and the middle button needs to navigate the Journal Stack, and depending on what screen in the Journal Stack the user is on, it needs to say and do different things.
const Tab = createBottomTabNavigator();
const TabBarIcon = ({ icon, title, focused }) => {
return (
<View style={styles.iconContainer}>
<FontAwesomeIcon
icon={icon}
color={focused ? Colors.neutral[4] : Colors.neutral[6]}
size={24}
style={styles.icon}
/>
<Text style={[styles.iconText, focused && styles.iconTextFocused]}>
{title}
</Text>
</View>
);
};
const NullScreen = () => null;
const TabNavigator = () => {
return (
<Tab.Navigator
initialRouteName="Journal"
screenOptions={({ route }) => ({
...defaultNavOptions,
headerShown: false,
tabBarStyle: { backgroundColor: Colors.neutral[3] },
tabBarShowLabel: false,
})}
>
<Tab.Screen
name="Learn"
component={LearnStackNavigator}
options={{
tabBarIcon: ({ focused }) => (
<TabBarIcon
focused={focused}
title={'Learn'}
icon={faUserGraduate}
/>
),
}}
/>
<Tab.Screen
name="Null Screen"
component={NullScreen}
options={{
tabBarButton: ({ focused }) => (
<View
style={{
position: 'relative',
bottom: 25,
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: 'grey',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
shadowColor: 'black',
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 3,
}}
>
<TouchableOpacity onPress={() => Alert.alert('hello world')}> // This is the button that I want use for useful things
<View style={[styles.iconContainer, styles.paddingBottom10]}>
<FontAwesomeIcon
icon={faPlus}
color={focused ? Colors.neutral[4] : Colors.neutral[6]}
size={32}
/>
<Text style={styles.iconText}>{'Add Sport'}</Text>
</View>
</TouchableOpacity>
</View>
),
}}
/>
<Tab.Screen
name="Journal"
component={LogbookStackNavigator}
options={{
tabBarIcon: ({ focused }) => (
<TabBarIcon focused={focused} title={'Journal'} icon={faPenAlt} />
),
}}
/>
</Tab.Navigator>
);
};
And here is what the LogbookStackNavigator looks like:
const LogbookStack = createStackNavigator();
const LogbookStackNavigator = () => {
return (
<LogbookStack.Navigator
screenOptions={{
...defaultNavOptions,
headerBackTitleVisible: false,
}}
>
<LogbookStack.Screen
name="Screen1"
component={screen1Component}
options={defaultNavOptions}
/>
<LogbookStack.Screen
name="Screen2"
component={screen2Component}
options={defaultNavOptions}
/>
<LogbookStack.Screen
name="Screen3"
component={screen3Component}
options={entryScreenOptions}
/>
<LogbookStack.Screen
name="Screen4"
component={screen4Component}
options={SaveLogbookScreenOptions}
/>
<LogbookStack.Screen
name="Screen5"
component={screen1Component5}
options={defaultNavOptions}
/>
</LogbookStack.Navigator>
);
};
I know how to use navigation.setOptions, but it only affects the immediate parent navigator, not the grandparent navigator.
Another thing I tried was to make the big circle button on the page itself, but it always rendered underneath the Tab Navigator. If there was a way to make it render above, I think I could just use that. I tried 'position: 'absolute', etc and it always rendered underneath the tab navigator. As it is, I had to basically make a dummy screen in the tab navigator to give me the button on top.
What I need to be able to do, is use big circle button on the Tab Navigator, to navigate to different screens in the LogbookStackNavigator. How do I do that?
Also, I need the title to change from "Add Sport" to "Add " depending on what screen the LogbookStackNavigator is on. How do I do that?
Thanks for your help
Finally figured this out. You have to use react-native-portalize. Just wrap the elements you want to be rendered on top in a
<Portal></Portal>. This will place it above a Bottom Tab navigator.
import { Portal } from 'react-native-portalize';
const FooterButton = () => {
return(
<Portal>
<View>
<Text>I appear above the Tab Navigator!</Text>
</View>
</Portal>
);
export default FooterButton;
Don't forget to wrap the whole app in the the Host:
//In app.js
import { Host } from 'react-native-portalize';
const App = () => {
return (
<Host>
<NavigationContainer>
<AppNavigator />
</NavigationContainer>
</Host>
)
}
export default App;
NOTE: The elements inside the Portal, do not clear when the navigator navigates to another screen. So to get around this, you have to only display the Portal, when the screen is active. Thankfully React Navigation 5+ provides a useIsFocused hook that accomplishes this perfectly.
import { Portal } from 'react-native-portalize';
import { useIsFocused } from '#react-navigation/native';
const FooterButton = () => {
const isFocused = useIsFocused();
// Only display the button when screen is focused. Otherwise, it doesn't go away when you switch screens
return isFocused ? (
<Portal>
<View style={styles.buttonContainer}>
<View style={styles.footer}>{props.children}</View>
</View>
</Portal>
) : null;
};
export default FooterButton;
If you want a modal-style popup, you can wrap react-native-modalize and wrap it with react-native-modalize
Thanks to livin52 on Reddit for the solution
I am trying to make a stack navigator using reactstack navigation. When the button clicks, it appears on the detail screen only, the title of the page is detail. I am not yet to parsing data array to the next screen, it just tries to navigate the screen into the detail screen, and gets this error. I am new to react. Please help me solve this problem.
import React from 'react'
import {Button, StyleSheet, ScrollView, Text, View} from 'react-native'
import {useState, useEffect} from 'react'
import axios from 'axios'
const Item = ({id, user_id, title, onPress, navigation}) => {
return (
<View style={styles.container}>
<Text style={styles.text}>Id :{id}
</Text>
<Text style={styles.text}>User Id :{user_id}
</Text>
<Text style={styles.text}>Tittle :{title}
</Text>
<View style={styles.container}>
<Button onPress={() => navigation.navigate('Detail')} title='Detail'></Button>
</View>
<View style={styles.line}></View>
</View>
)
}
const Berita = () => {
const [users,
setUsers] = useState([]);
useEffect(() => {
getData();
}, []);
const selectItem = (item) => {
console.log('Selected item: ', item)
}
const getData = () => {
axios
.get('https://gorest.co.in/public/v1/posts')
.then(res => {
console.log('res: ', res);
setUsers(res.data.data);
})
}
return (
<ScrollView style={styles.container}>
{users.map(user => {
return <Item key={user.id} id={user.id} user_id={user.user_id} title={user.title}/>
})}
</ScrollView>
)
}
export default Berita
const styles = StyleSheet.create({
container: {
padding: 15
},
text: {
color: "black",
marginTop: 5,
fontStyle: 'italic',
fontSize: 18,
fontFamily: 'Arial'
},
line: {
height: 1,
backgroundColor: 'black',
marginVertical: 20
},
title: {
fontSize: 25,
fontWeight: 'bold',
textAlign: 'center',
color: "black"
},
tombol: {
padding: 10
}
})
This is the stack screen navigator code
const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();
const DetailBerita = () => {
return (
<Stack.Navigator >
<Stack.Screen
name='Berita'
component={Berita}
options={{
headerTitleAlign: 'center'
}}/>
<Stack.Screen
name="Detail"
component={Detail}
options={{
headerTitleAlign: 'center'
}}/>
</Stack.Navigator>
)
}
It appears that you are using Stack Navigators with different screen names, but you didn't send it. If possible, can you send that file, I would be able to help you a bit better. But from what I have I can try and explain how Stack navigation works. With this line of code:
navigation.navigate('Detail')
You specify that you want to navigate to the screen named "Detail", if you want to navigate to another screen then you can change it to the name of the screen. Let's say you want to navigate to your home screen, and its named "Home" in your Stack.Navigation component. Simply change your navigation to the following:
navigation.navigate('Home')
This is happening because the navigation prop is passed to the Berita component and you are destructuring the property in Item component not in Berita.
So the code should look like
...
const Berita = ({ navigation }) => {
// ...
return (
<ScrollView style={styles.container}>
{users.map(user => {
return (
<Item
key={user.id}
id={user.id}
user_id={user.user_id}
title={user.title}
navigation={navigation} // Pass navigation
/>
);
})}
</ScrollView>
);
};
Another way is - you can just use navigation in onPress (Berita) and pass down onPress to Item component
const Item = ({ id, user_id, title, onPress }) => {
return (
<View style={styles.container}>
<Text style={styles.text}>Id :{id}</Text>
<Text style={styles.text}>User Id :{user_id}</Text>
<Text style={styles.text}>Tittle :{title}</Text>
<View style={styles.container}>
<Button onPress={onPress} title="Detail" />
</View>
<View style={styles.line}></View>
</View>
);
};
const Berita = ({ navigation }) => {
const [users, setUsers] = useState([]);
useEffect(() => {
getData();
}, []);
const selectItem = item => {
console.log('Selected item: ', item);
};
const getData = () => {
axios.get('https://gorest.co.in/public/v1/posts').then(res => {
console.log('res: ', res);
setUsers(res.data.data);
});
};
return (
<ScrollView style={styles.container}>
{users.map(user => {
return (
<Item
key={user.id}
id={user.id}
user_id={user.user_id}
title={user.title}
onPress={() => navigation.navigate('Detail')}
/>
);
})}
</ScrollView>
);
};
I am working on a hobby gym management app, and I am puzzled by the mechanism of sharing state between three components in React-Native.
My three components are:
1. Schedule:
[...]
function Schedule() {
return (
<Stack.Navigator
initialRouteName="Monday"
screenOptions={{
headerStyle: { backgroundColor: "#f58220" },
headerTintColor: "#fff",
headerTitleStyle: { fontWeight: "bold" },
headerRight: () => <SwitchButton />,
}}
>
<Stack.Screen
name="TabStack"
component={TabStack}
options={{ title: "Aerobic Schedule" }}
/>
</Stack.Navigator>
);
}
export default Schedule;
I want the SwitchButton button in my Schedule component (1.) to alternate between DATA_AEROBIC and DATA_KIDS arrays props of the FlatList in (2.) based on the content of the listAerobic boolean variable.
2. MondayPage:
[...]
const MondayPage = () => {
const [selectedId, setSelectedId] = useState(null);
const [listAerobic, setListAerobic] = useState(true);
const renderItem = ({ item }) => {
const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff";
return (
<Item
item={item}
onPress={() => setSelectedId(item.id)}
style={{ backgroundColor }}
/>
);
};
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1, padding: 5 }}>
<SafeAreaView style={styles.container}>
<FlatList
data={listAerobic ? DATA_AEROBIC : DATA_KIDS}
renderItem={renderItem}
keyExtractor={(item) => item.id}
extraData={selectedId}
/>
</SafeAreaView>
</View>
</SafeAreaView>
);
};
However, I don't know how to link the listAerobic boolean variable to the state of the SwitchButton component (3.) , and how to make it toggle on and off.
3. SwitchButton:
const SwitchButton = () => {
const [isEnabled, setIsEnabled] = useState(false);
const toggleSwitch = () => setIsEnabled(previousState => !previousState);
return (
<View style={styles.container}>
<Switch
trackColor={{ false: "#767577", true: "#81b0ff" }}
thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
<Text> aerobic/kids</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
marginRight: 5,
padding: 5,
}
});
export default SwitchButton;
Any guidance would be awesome! I mention I have really tried to look it up on different tutorials, but I can't seem to get the gist of it. It is my first project in React/React-Native.
Many thanks!
I think you just need 'value' to accept a prop passed into it on the switch button. Then wherever you use switch button just pass a boolean value into it from state e.g.
<SwitchButton enabled={this.state.switchEnabled}/>
As for setting state 'globally' so this.state.switchEnabled can be updated from various places / accessible all over the app you need to look into state management tools like Redux (or I hear 'React Hooks' is now a thing and preferred....)
I am using createDrawerNavigator and inside this I made a custom profile view. When I press navigate I want to move to another screen.
But this give me an error undefined is not an object (evaluating"_this.props.navigation"). Other components one, two, three works fine.
This is my code:
export default class App extends Component {
render() {
return (
<View style={{flex:1, marginTop:30}}>
<AppContainer/>
</View>
);
}
}
const CustomDrawerContentComponent = (props)=> {
return(
<SafeAreaView style={{flex:1}}>
<View style={{ height:100, backgroundColor: '#9FA8DA' }}>
<Image
style={{marginLeft:20,height:100,width:100,borderRadius:50}}
source={require('./assets/puppy.jpg')}/>
</View>
<View style={{flexDirection:'row', margin:20, alignItems:'center'}}>
//////here is where I get an error
<TouchableOpacity
style={{marginTop:0}}
onPress={()=> {this.props.navigation.navigate('profile')}}>
<Text style={{fontSize:18, fontWeight:'bold'}}>navigate</Text>
<Image
style={{height:12,width:12}}
source={require('./assets/drawable-hdpi/ic_arrow_depth.png')}/>
</TouchableOpacity>
</View>
<ScrollView>
<DrawerItems {...props}/>
</ScrollView>
</SafeAreaView>
)
}
const AppDrawerNavigator = createDrawerNavigator({
one: AppStackNavigator,
two: BoardScreen,
three: NotificationScreen,
},{
contentComponent:CustomDrawerContentComponent,
})
const AppStackNavigator = createStackNavigator({
profile: {
screen: profileScreen,
navigationOptions: {
header: null
},
},
})
const StartSwitchNavigator = createSwitchNavigator(
{
App: AppDrawerNavigator,
},
{
initialRouteName: 'App',
}
)
Your CustomDrawerContentComponent component is functional component. Use props.navigation directly instead of this.props
i want to create rows for images,which recieve from _find function.This function already seperated array to subarrays,which number equals number of rows,how i can render rows with data from _find?Dont purpose ready solutions such as react-native-easy-grid,i want to do it without another libs,and can i scroll items if i use this way?
import React, { Component } from 'react';
import { AppRegistry, Text, TextInput, View,StyleSheet,Button,Image,ScrollView,Dimensions,ListView } from 'react-native';
import Grid from './GridBuilder.js';
const regUrl = /(src=")?(https:\/\/\S{2,500})(")/gm;
var IMAGES_PER_ROW = 3;
let app1;
export default class inputText extends Component {
constructor(props) {
super(props);
app1 = this;
this.state = {
text: null,
findEvent:false,
im_p_r:3,
items:{},
};
}
render() {
return (
<View style={{margin: 20}}>
<TextInput
style = {styles.searchInput}
placeholder="Type here to search"
onChangeText={(text) => this.setState({text})}
/>
<Button
onPress={() => this._find(this.state.text)}s
title='Find'
color="#841584"
accessibilityLabel="on"
/>
{this.state.findEvent && <DisplayImage />}
</View>
);
}
_find(searchText){
fetch('https://www.googleapis.com/customsearch/v1?key=AIzaSyAfcN3jfimFxHxpHNjhHOSuuY8dm5YZnqQ&cx=007223195539364418542:lcqjo0djp7k&num=10&q='+ searchText+'&searchType=image')
.then((resp) => resp.json())
.then(function(data) {
let s = data.items;
let SIZE = IMAGES_PER_ROW;
let res = s.reduce((p,c)=>{
if(p[p.length-1].length == SIZE){
p.link.push([]);
}
p[p.length-1].push(c);
return p.link;
}, [[]])
app1.setState({items:res,findEvent:true});
})
}
}
export class DisplayImage extends Component {
render(){
return(
<View style={styles.container}>
{app1.state.items.map((item,index) => <View style={styles.row} ><Image style={[styles.image,styles.box]} source={{uri:item.link}} key={index} /></View>)}
</View>
)
}
}
const styles = StyleSheet.create({
searchInput:{
fontSize:20,
paddingTop:20,
paddingBottom:20
},
image:{
paddingTop:20,
width:100,
height:100,
},
row: {
flex: 1,
flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'space-between'
},
box: {
flex: 1,
height: 100,
width:100,
backgroundColor: '#333',
},
})
AppRegistry.registerComponent('inputText', () => inputText);
AppRegistry.registerComponent('DisplayImage', () => DisplayImage);
You can use FlatList from React Native.
{this.state.findEvent && <FlatList
data={this.state.items}
renderItem={({ item }) => this.renderItem(item)}
/>}
FlatList receive as data the list of elements you want to render, in this case the data returned from the find function.
And then define the renderItem function like:
renderItem(item) {
return (
<View style={styles.row} >
<Image
style={[styles.image,styles.box]}
source={{uri:item.link}} key={index}
/>
</View>
);
}
This function is in charge of rendering the list of images, each image as a row as you want.
FlatList is really useful and makes lists rendering easier. You get the scroll by default and you can also render separators, have a pull to refresh, etc. Take a look to the FlatList doc to see all properties available.
Here is working example of Flat list by which you can get images in the row
https://snack.expo.io/SJDoljDg7
FlatList is the way to go but I suspect the spec has changed since the original accepted answer. You must now provide a key extractor, here is an example of what worked for me:
const listItems = [
{
"id": 0.7967679550647925,
"name": "Hcsrhjkh",
},
{
"id": 0.3212834674770011,
"name": "Phvdgbb",
},
{
"id": 0.30092504022778455,
"name": "Hnvdghbh",
},
]
...
{listItems.length < 1 ? (
<Text style={{ fontSize: 30 }}>Your list is empty.</Text>
) : (
<FlatList
data={listItems}
renderItem={({item}) => <ListItem item={item} />}
keyExtractor={(item) => item.id.toString()}
/>
)}
As you can might have found, the keyExtractor expects a string so I've coerced the 'id' which is a number to a string.