React native FlatList not rerendering when data prop changes to empty array - react-native

I have a FlatList with a data prop pulling from Redux
render() {
return (
<View>
<FlatList
data={this.props.arrayOfPlacesFromRedux}
renderItem={({item}) => {.......
Whenever I dispatch changes to arrayOfPlacesFromRedux(i.e. adding or removing children), the FlatList rerenders....UNLESS I remove all children from array (i.e. make length zero).When arrayOfPlacesFromRedux changes from a positive length to a length of zero, the FlatList does not rerender.....however all other types of changes to array do indeed cause FlatList to rerender
UPDATE 02/27
Below is my reducer used to update Redux arrayOfPlacesFromRedux
const reducer = (state = initialState, action) => {
switch (action.type) {
case UPDATE_PLACES_ARRAY:
return {...state, arrayOfPlaces: action.payload};
default:
return state;
}
};
In the situation noted above when FlatList does not rerender.....action.payload is an empty array

The question is missing some important piece of code.
React as well as Redux need arrays reference to change, meaning for a component to reRender on state change, the array references needs to change.
Live demo at https://snack.expo.dev/RrFFxfeWY
Here is the most interesting parts:
If you have a basic component as below:
const MyList = () => {
const [data, setData] = React.useState([
'#FF0000',
'#FF8000',
'#FFFF00',
]);
return (
<>
<Text>List poping is not working</Text>
<FlatList
data={data}
renderItem={({ item }) => (
<Pressable
onPress={() => {
data.pop(); // Does not work because we are not changing it's ref
}}
style={{ backgroundColor: item, padding: 8 }}>
<Text>{item}</Text>
</Pressable>
)}
/>
</>
);
};
The data need to have a new array reference as below. data2.filter(..) will return a new array, we are not changing the data2 base values, just creating a new array with one item less.
const MyList = () => {
const [data2, setData2] = React.useState([
'#00FFFF',
'#0080FF',
'#0000FF',
]);
return (
<>
<Text>List WORKING!</Text>
<FlatList
data={data2}
renderItem={({ item }) => (
<Pressable
onPress={() => {
setData2(data2.filter(dataItem => dataItem !== item)) // works
//setData2([]); // Also works
}}
style={{ backgroundColor: item, padding: 8 }}>
<Text>{item}</Text>
</Pressable>
)}
/>
</>
);
};
A library like Immer.js simplify the manipulation of states to mutate the object, and immer will created a new reference for you.

Oh no rookie mistake that wasted everyones time!!
I was implementing shouldComponentUpdate method that was stopping Flatlist rendering :(
Thanks for all for the answers

You may need to use ListEmptyComponent, which is a prop that comes with FlatList, src.
Honestly, I'm not sure why it does not re-render when you update your state, or why they added a specific function/prop to render when the array is empty, but it's clear from the docs that this is what's needed.
You can do something like this:
<SafeAreaView style={styles.container}>
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => item.id}
extraData={selectedId}
--> ListEmptyComponent={() => <MyComponent />}
/>
</SafeAreaView>

Related

React native Flatlist not re-rendering on state change

I realize there are a lot of questions and answers about this out there but I am fairly new to react native and most of the answers are dealing with React Components and not hooks. In the following example availableInterests is pulled from a firestore database call. Then we loop through the availableInterests so the user can select the their interests from the Flatlist of interests. Everything works great except the FLatlist does not re-render so the button that is used to select currentInterests never shows the change that an interest has been selected. Does anyone see what I am missing here?
const [availableInterests, setAvailableInterests] = useState([]);
const [currentInterests, setCurrentInterests] = useState([]);
const selectThisInterest = (item) => {
let myInterests = currentInterests;
if(myInterests.includes(item.id)) {
myInterests.pop(item.id);
} else {
myInterests.push(item.id);
}
setCurrentInterests(myInterests);
}
return <View>
<Text style={styles.text}>Select Your Interests:</Text>
<FlatList
data={availableInterests}
keyExtractor={(item, index) => index.toString()}
extraData={currentInterests}
renderItem={({ item, index }) =>
<View key={item.id}>
<Text>{item.title}</Text>
<Text>{item.description}</Text>
<Image
source={{ uri: item.icon }}
style={{ width: 100, height: 100}}
/>
<TouchableOpacity onPress={() => selectThisInterest(item)}>
<Text style={styles.buttonText}>{`${currentInterests.includes(item.id) ? 'UnSelect' : 'Select'}`}</Text>
<Text>{item.id}</Text>
</TouchableOpacity>
</View>
}>
</FlatList>
</View>
put this state below
const [currentInterests, setCurrentInterests] = useState([]);
const [extra, setExtra] = useState(0);
at the end of your function just put this
const selectThisInterest = (item) => {
....
setExtra(extra + 1)
}
I think the mistake is in your selectThisInterest function. When you are updating the currentInterests based on previous value, React doesn't recognises such a change because you are simply assigning myInterests with your currentInterests.
What you want to do is to copy that array and assign it to myInteresets and then update your values to the new copied array. Once the calculation are completed on the new myInteresets array, the setCurrentInterests() will re-render the app because now React recognises there is a change in the state.
To copy the array, you can use,
let myInterests = [...currentInterests];
change your selectThisInterest function to reflect this change,
const selectThisInterest = (item) => {
let myInterests = [...currentInterests];
if(myInterests.includes(item.id)) {
myInterests.pop(item.id);
} else {
myInterests.push(item.id);
}
setCurrentInterests(myInterests);
}

FlatList not rendering style dynamically

I'm currently struggling in making my FlatList applying the changes I do to it. What I am wanting right now is that when I click an item in my flatlist, that it highlights in a certain color. I followed an approach done by a guy but I am having the problem that to me is not working the update once I click.
I can see through console that all I am doing performs a modification but I think that I am missing some point with extraData parameter since it is not re-rendering with the backgroundColor that I would like to apply.
The code I have is as following, I know that the style I am applying is correct since if i substitute in the map styles.list per styles.selected, everything gets the background I would like to be applied to the elements I click.
So summarizing, the issue I think I have is that the flatlist is not re-rendering so it doesn't show the modifications I perform on it. Any idea of what I am doing wrong? Any tip?
render() {
const { students, studentsDataSource, loading, userProfile } = this.props.navigation.state.params.store;
this.state.dataSource = studentsDataSource._dataBlob.s1.map(item => {
item.isSelect = false;
item.selectedClass = styles.list;
return item;
})
const itemNumber = this.state.dataSource.filter(item => item.isSelect).length;
return (
<View style={styles.container}>
<Item rounded style={styles.searchBar}>
<Input placeholder='Group Name'/>
</Item>
<FlatList
style={{
flex: 1,
width: "100%",
}}
data={this.state.dataSource}
ItemSeparatorComponent={this.FlatListItemSeparator}
renderItem={ ({ item }) => (
<ListItem avatar style={[styles.list, item.selectedClass]}
onPress={() => this.selectItem(item)}>
<Left>
{!item.voteCount && <Avatar unseen={true} /> }
{!!item.voteCount > 0 && <Avatar />}
</Left>
<Body>
<Text>{item.name}</Text>
<Text note>{item.group}</Text>
</Body>
</ListItem>
)
}
listKey={item => item.key}
extraData={this.state}
/>
</View>
);
}
Here we can find the state and SelectItem functions:
constructor(props) {
super(props)
this.state = {
dataSource : [],
}
}
//FlatListItemSeparator = () => <View style={styles.line} />;
selectItem = data => {
//{console.log("inside SelectItem=", data)}
data.isSelect = !data.isSelect;
data.selectedClass = data.isSelect? styles.selected: styles.list;
const index = this.state.dataSource.findIndex( item => data.key === item.key);
this.state.dataSource[index] = data;
this.setState({
dataSource: this.state.dataSource,
});
console.log("This state has the changes:=",this.state.dataSource)
};
Well the main issue was that I was not using the .setState and instead I was doing assignations which killed the listeners.

Conditionally style not working in react native

I followed this answer to dynamically style my component.
Here is my render method :
render() {
return (
<View style={styles.container}>
<FlatList
data={this.state.images}
numColumns={2}
keyboardShouldPersistTaps={'always'}
keyboardDismissMode={'on-drag'}
keyExtractor={item => item.localIdentifier}
renderItem={({ item, index }) =>
<TouchableHighlight
underlayColor='transparent'
onPress={() => this.openImage(index)}
onLongPress={() => this.startSelection(item)}
>
<View style={[styles.albumContainer, (this.state.selectedItems.indexOf(item)>-1)?styles.selectedItem:styles.unselectedItem]}>
<Image
style={styles.albumThumbnail}
source={item.image}
/>
</View>
</TouchableHighlight>
}
/>
</View>
);
}
As you can see I am displaying image thumbnail with TouchableHighlight and FlatList. When user will press and hold on any image thumbnail I called startSelection() with particular flatlist item which then add that item to state. I used that state to set style dynamically of my image as :
<View style={[styles.albumContainer, (this.state.selectedItems.indexOf(item)>-1)?styles.selectedItem:styles.unselectedItem]}>
<Image
style={styles.albumThumbnail}
source={item.image}
/>
</View>
Here is startSelection() method :
startSelection(item) {
let temp = this.state.selectedItems;
temp.push(item);
this.setState({
selectedItems : temp
});
}
Here is my stylesheet :
const styles = StyleSheet.create({
selectedItem: {
borderWidth: 3,
borderColor: '#22aaff',
},
unselectedItem: {
borderColor: '#000000',
}
});
But when user press and hold that view, item will added to state but style is not changing.
Please help me what's going wrong here !!!
This can be found on FlatList docs:
This is a PureComponent which means that it will not re-render if props remain shallow-equal. Make sure that everything your renderItem function depends on is passed as a prop (e.g. extraData) that is not === after updates, otherwise your UI may not update on changes. This includes the data prop and parent component state.
So you can add extraData to your FlatList component like this:
FlatList Component:
<FlatList
data={this.state.images}
extraData={this.state} //add this!
numColumns={2}
keyboardShouldPersistTaps={'always'}
keyboardDismissMode={'on-drag'}
keyExtractor={item => item.localIdentifier}
renderItem={({ item, index }) =>
<TouchableHighlight
underlayColor='transparent'
onPress={() => this.openImage(index)}
onLongPress={() => this.startSelection(item)}
>
<View style={[styles.albumContainer, (this.state.selectedItems.indexOf(item)>-1)?styles.selectedItem:styles.unselectedItem]}>
<Image
style={styles.albumThumbnail}
source={item.image}
/>
</View>
</TouchableHighlight>
}
/>
P.S: If your component state has variables which should not re-render FlatList, you would be better of using extraData = {this.state.selectedItems}, but then you need to make sure you pass a different reference to selectedItems when you call setState on startSelection. Like this:
startSelection(item) {
let temp = [...this.state.selectedItems];
temp.push(item);
this.setState({
selectedItems : temp
});
}
Wrap them with extra []
style={[styles.albumContainer, [(this.state.selectedItems.indexOf(item)>-1)?styles.selectedItem:styles.unselectedItem]]}

React Native: Lodash Map

I'm new in react native and I want make data function into view.
my function looks like this
renderTest = () => {
<FlatList
onEndReached={0}
onEndReached={() => this.handleEnd()}
>
{_.map(this.state.leads, (leads, index) => {
return (
<Text key={index}>{leads.full_name}</Text>
)
})}
</FlatList>
}
and my View to pass the value of the function
<View style={{flexDirection: 'row'}}>
{this.renderTest()}
</View>
I don't have any idea what's the problem all I just want is to render the value. I hope could someone help me.
edited
Since you already are in react world you can simply use the Array.map:
renderTest = () => {
<FlatList onEndReached={() => this.handleEnd()}>
{this.state.leads.map((lead, index) => {
return (<Text key={index}>{lead.full_name}</Text>)
})}
</FlatList>
}
As long as this.state.leads is an array.
But bottom line is no lodash is needed here.
As seen by your comment on Akrion's answer, I'm guessing you haven't defined the renderTest function inside the component. Another possibility is that you are using a stateless component in which case you cannot access this
I'm just not sure you can define a FlatList like that one. I mean, I think you're missing the return of the function and you have to pass the property data to FlatList this way:
renderTest = () => {
return (
<FlatList
onEndReached={0}
onEndReached={() => this.handleEnd()}
data={this.state.leads}
renderItem={(lead, index) => <Text key={index}>{lead.full_name}</Text>}
/>
)
}
Let me know if this can solve your issue :)

ScrollToEnd after update data for Flatlist

I'm making a chat box with Flatlist. I want to add a new item to data then scroll to bottom of list. I use scrollToEnd method but it did not work. How can I do this?
<FlatList
ref="flatList"
data={this.state.data}
extraData = {this.state}
renderItem={({item}) => <Text style={styles.chatFlatListItem}>{item.chat}</Text>}
/>
AddChat(_chat){
var arr = this.state.data;
arr.push({key: arr.length, chat: _chat});
var _data = {};
_data["data"] = arr;
this.setState(_data);
this.refs.flatList.scrollToEnd();
}
I found a better solution,scrollToEnd() is not working because it is triggered before the change is made to the FlatList.
Since it inherits from ScrollView the best way here is to call scrollToEnd() in onContentSizeChange like so :
<FlatList
ref = "flatList"
onContentSizeChange={()=> this.refs.flatList.scrollToEnd()} />
Thanks #Kernael, just add a timeout like so:
setTimeout(() => this.refs.flatList.scrollToEnd(), 200)
const flatList = React.useRef(null)
<FlatList
ref={flatList}
onContentSizeChange={() => {
flatList.current.scrollToEnd();
}}
data={this.state.data}
extraData = {this.state}
renderItem={({item}) => <Text style={styles.chatFlatListItem}>{item.chat}</Text>}
/>
try this,it works.
My issue here was that scrollToEnd() worked fine on mobile but on web it always scrolled to the top. Probably because I have elements with different size in the FlatList and couldn't define getItemLayout. But thanks to the accepted answer here I solved it. Just with different approach.
const ref = React.useRef<FlatList>();
function handleScrollToEnd(width, height) {
if (ref.current) {
ref.current.scrollToOffset({offset: height});
}
}
<FlatList
ref={ref}
onContentSizeChange={handleScrollToEnd}
/>
This works great on both the mobile and web. Hope it helps to somebody.
Change your code as below. The ref is modified and It's better to use getItemLayout in your FlatList according to this.
AddChat(_chat){
var arr = this.state.data;
arr.push({key: arr.length, chat: _chat});
var _data = {};
_data["data"] = arr;
this.setState(_data);
this.flatList.scrollToEnd();
}
<FlatList
ref={elm => this.flatList = elm}
data={this.state.data}
extraData = {this.state}
renderItem={({item}) => <Text style={styles.chatFlatListItem}>{item.chat}</Text>}
getItemLayout={(data, index) => (
{length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
)}
/>
Note: Replace the ITEM_HEIGHT with the real value of height of your list items.
Try to use inverted prop on Fatlist itself
Pass your data like this [...data].reverse()
If you are at the middle of list and you need to scroll to end when a new item is added, just use:
ref => flatlistRef.current?.scrollToOffset({offset:0})
seems caused by this line
https://github.com/facebook/react-native/blob/3da3d82320bd035c6bd361a82ea12a70dba4e851/Libraries/Lists/VirtualizedList.js#L1573
when use trigger scrollToEnd, frame.offset is 0
https://github.com/facebook/react-native/blob/3da3d82320bd035c6bd361a82ea12a70dba4e851/Libraries/Lists/VirtualizedList.js#L390
if you wait 1 second, _onContentSize changes and frame.offset is valorized (for ex. 1200 px).
Related post https://github.com/facebook/react-native/issues/30373#issuecomment-1176199466
Simply add a loader before Flatlist renders. For example:
const flatListRef = useRef(null);
const [messages, setMessages] = useState([]);
if(!messages.length){
return <Loader />
}
return (
<View style={styles.messagesContainer}>
<FlatList
ref={flatListRef}
data={messages}
onContentSizeChange={() => {
if (flatListRef.current) {
flatListRef?.current?.scrollToEnd();
}
}}
renderItem={({item, index}) => {
return (
<DisplayMessages
message={item}
index={index}
/>
);
}}
/>
</View>