I have a FlatList that holds 5000 items. There is an option to switch on the "Select Mode", which renders a checkbox next to each item. Whenever I press the corresponding button to switch to that mode, there is a noticeable delay before checkboxes are actually rendered.
This is my state:
{
list: data, // data is taken from an external file
selectedItems: [] // holds ids of the selected items,
isSelectMode: false
}
My list:
<FlatList
data={list}
extraData={this.state}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
removeClippedSubviews
/>
My list item:
<ListItem
title={item.title}
isCheckboxVisible={isSelectMode}
isChecked={isChecked}
onPress={() => this.toggleItem(item)}
/>
Each list item implements shouldComponentUpdate:
...
shouldComponentUpdate(nextProps) {
const { isCheckboxVisible, isChecked } = this.props;
return isChecked !== nextProps.isChecked || isCheckboxVisible !== nextProps.isCheckboxVisible;
}
...
I always thought that FlatList only renders items that a currently visible within the viewport, but here it feels like the entire list of 5000 items is re-rendered. Is there anything I can improve here? Or perhaps I am doing something totally wrong?
Full code: https://snack.expo.io/#pavermakov/a-perfect-flatlist
Related
I'm building a React-Native app and trying to optimize it, i runned into the case of my Flatlist.
So this Flatlist basically renders few elements and each of these elements are selectable.
The issue i'm facing is that selecting one single item rerenders the whole Flatlist, and thus all items it contains.
I've seen a lot of solutions online already, and tried them without any success.
Here is my code :
Class component containing the Flatlist
const keyExtractor = (item) => item.id
export default class OrderedList extends Component {
state = {
selected: null,
}
onPressSelect = (id) => {
console.log(this.state.selected)
if(this.state.selected === id) {
this.setState({ selected: null})
}
else {
this.setState({ selected: id})
}
}
renderItemOrdered = ({item}) => {
const { group, wording, description, id: uniqueID } = item
const { id, name } = group
return (
<CategoryCard
type="ordered"
// item={item}
uniqueID={uniqueID}
groupName={name}
groupID={id}
description={description}
title={wording}
selected={this.state.selected}
onPressSelect={() => this.onPressSelect(item.id)}
/>
)
}
render() {
return (
<FlatList
initialNumToRender={10}
maxToRenderPerBatch={10}
data={this.props.data}
renderItem={this.renderItemOrdered}
keyExtractor={keyExtractor}
extraData={this.state.selected} ---> Tried with and without it
/>
)
}
}
Class component containing the renderItem method
export default class CategoryCard extends Component {
shouldComponentUpdate = (nextProps, nextState) => {
return nextProps.selected !== this.props.selected &&
nextProps.onPressSelect !== this.props.onPressSelect
}
render(){
if(this.props.type === 'ordered') {
return (
<Pressable style={this.props.selected === this.props.uniqueID ? styles.cardContainerSelected : styles.cardContainer} onPressIn={this.props.onPressSelect}>
<View style={[styles.cardHeader, backgroundTitleColor(this.props.groupID)]}>
<Text style={[styles.cardGroupName, textTitleColor(this.props.groupID)]}>{this.props.groupName}</Text>
</View>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{this.props.wording}</Text>
<Text style={styles.cardDescription} numberOfLines={3} ellipsizeMode="tail">{this.props.description}</Text>
</View>
</Pressable>
)
}
}
}
What i already tried :
At first my components were functional components so i changed them into class components in order to make things works. Before that, i tried to use React.memo, also to manually add a function areEqual to it, to tell it when it should rerender, depending on props.
It didn't give me what i wanted.
I also tried to put all anonymous functions outside return statements, made use of useCallback, played around the ShouldComponentUpdate (like adding and removing all the props, the onPress prop, selected props)... None of that worked.
I must be missing something somewhere.. If you can help me with it, it would be a big help !
I have a screen (parent) where a FlatList resides in, and the renderItem shows a Child element.
In this Child element, I have a Pressable, and when the user clicks on it, it shows a Checked Icon and changes its background colour.
This happens dynamically based off a state Array in the Parent where in each renderItem of the Child I pass the state Array.
And in the Child component I check if the ID of this Child element is present, if it is, a Checked Icon is shown and the background changes colour.
I know that states in React is asynchronous but I'm always having problems working through such scenarios.
I have tried checking in the Parent screen where the FlatList resides at, to instead pass a Boolean prop to the Child on whether to show the Checked Icon.
E.g. (Sorry always having trouble formatting code in SO)
<FlatList
data={displayData}
renderItem={({item}) => (
<Child
key={item}
userData={item}
id={item}
isSelected={selectedIds?.includes(item)}
// selectedIds={selectedIds}
selectedHandler={id => selectedHandler(id)}
/>
)}
keyExtractor={item => item}
/>
instead of
// In Parent Screen
<FlatList
data={displayData}
renderItem={({item}) => (
<Child
key={item}
userData={item}
id={item}
selectedIds={selectedIds} // here
selectedHandler={id => selectedHandler(id)}
/>
)}
keyExtractor={item => item}
/>
// In Child element
const Child = ({
id,
selectedIds,
selectedHandler
}) => {
return (
<Pressable
style={[
styles.checkContainer,
selectedIds?.includes(id) && { backgroundColor: '#3D9A12' }
]}
onPress={onPressHandler}
>
{selectedIds?.includes(id) && <CheckIcon />} {/* Problem lies here. Not showing Checked Icon */}
</Pressable>
);
};
I won't dump any code here as I have made a snack of the reproduction of my problem.
I appreciate any help please. Thank you so much
Unchecked:
Checked:
The problem is in the selectedHandler function.
You are storing the reference of your state in this variable.
let selectedArr = selectedIds;
and later directly modifying the state itself by doing so:
selectedArr.push(id);
This is why the state updation is not firing the re-render of your component.
Instead, what you need to do is:
let selectedArr = [...selectedIds];
By spreading it, you will be storing a copy of your array and not a reference to it. Now if you modify selectedArr, you won't modifying your state.
I made the changes in the snack provided by you and it now works fine.
The updated selectedHandler function:
const selectedHandler = id => {
let selectedArr = [...selectedIds];
console.log('before selectedArr', selectedArr);
if (selectedArr.includes(id)) {
selectedArr = selectedArr.filter(userId => userId !== id);
setSelectedIds(selectedArr);
console.log('after selectedArr', selectedArr);
return;
}
if (selectedArr.length > 2) {
selectedArr.shift();
}
selectedArr.push(id);
console.log('after selectedArr', selectedArr);
setSelectedIds(selectedArr);
};
I'm learning React. Say that I have a ListContainer which renders several ListItems. I want to keep track of the currently selected ListItem, render it in another color, and be able to navigate up and down.
One way would be to store selectedItem as state in ListContainer, and send it down as a prop to ListItem. But if I do it this way, then every time I change selectedItem I will rerender all ListItems because they are dependent on selectedItem. (I should only have to re-render two ListItems, the one that gets deselected, and the one that gets selected).
Is there a way to implement next and previous function without re-rendering all items?
Note: I know that React doesn't re-render unnecessarily in the DOM, but I'm trying to optimize operations on virtual DOM also.
Edit: Here is my example in code. It renders a list, and when the user click one item it gets selected. We also see that "ListItem update" gets printed 100 times, each time we change selection, which happens regardless of PureComponent or React.memo.
let mylist = []
for (let i = 0; i < 100; i++) {
mylist.push({ text: "node:" + i, id: i })
}
window.mylist = mylist
const ListItem = React.memo (class extends Component {
componentDidUpdate() {
console.log('ListItem update')
}
render() {
let backgroundColor = this.props.item.id === this.props.selectedItem ? 'lightgreen' : 'white'
return (
<li
style={{ backgroundColor }}
onMouseDown={() => this.props.setSelected(this.props.item.id)}
>
{this.props.item.text}
</li>
)
}
})
class ListContainer extends Component {
constructor(props) {
super(props)
this.state = {
selectedItem: 10
}
this.setSelected = this.setSelected.bind(this)
}
setSelected(id) {
this.setState({ selectedItem: id })
this.forceUpdate()
}
render() {
return (
<ul>
{this.props.list.map(item =>
<ListItem
item={item}
key={item.id}
selectedItem={this.state.selectedItem}
setSelected={this.setSelected}
/>)}
</ul>
)
}
}
function App() {
return (
<ListContainer list={mylist} />
);
}
The state you suggesting is the right way to implement it...
The other problem with unnecessary renders of the list item can easily be soved by wrapping the export statement like this:
export default React.memo(ListItem)
This way the only elements that has changed their props will rerender.. be aware that overuse of This can cause memory leaks when using it unnecessarily...
UPDATE
according to your example in addition to the React.memo you can update the way you transfer props to avoid senfing the selected item in each item...
istead of:
let backgroundColor = this.props.item.id === this.props.selectedItem ? 'lightgreen' : 'white'
...
<ListItem
item={item}
key={item.id}
selectedItem={this.state.selectedItem}
setSelected={this.setSelected}
/>)}
do :
let backgroundColor = this.props.selectedItem ? 'lightgreen' : 'white'
...
<ListItem
item={item}
key={item.id}
selectedItem={item.id === this.state.selectedItem}
setSelected={this.setSelected}
/>)}
this way the react memo will prevent rerenders when it is possible...
I have created the following component:
type ToggleButtonProps = { title: string, selected: boolean }
export default class ToggleButton extends Component<ToggleButtonProps>{
render(){
return (
<TouchableWithoutFeedback {...this.props}>
<View style={[style.button, this.props.selected ? style.buttonSelected : style.buttonDeselected]}>
<Text style={[style.buttonText, this.props.selected ? style.buttonTextSelected : style.buttonTextDeselected]}>{this.props.title}</Text>
</View>
</TouchableWithoutFeedback>
);
}
}
The styles are simple color definitions that would visually indicate whether a button is selected or not. From the parent component I call (item is my object):
item.props.selected = true;
I've put a breakpoint and I verify that it gets hit, item.props is indeed my item's props with a selected property, and it really changes from false to true.
However, nothing changes visually, neither do I get render() or componentDidUpdate called on the child.
What should I do to make the child render when its props change? (I am on React Native 0.59.3)
You can't update the child component by literally assigning to props like this:
item.props.selected = true;
However, there are many ways to re-render the child components. But I think the solution below would be the easiest one.
You want to have a container or smart component which will keep the states or data of each toggle buttons in one place. Because mostly likely, this component will potentially need to call an api to send or process that data.
If the number of toggle buttons is fixed you can simply have the state like so:
state = {
buttonOne: {
id: `buttonOneId`,
selected: false,
title: 'title1'
},
buttonTwo: {
id: `buttonTwoId`,
selected: false,
title: 'title2'
},
}
Then create a method in the parent which will be called by each child components action onPress:
onButtonPress = (buttonId) => {
this.setState({
[buttonId]: !this.state[buttonId].selected // toggles the value
}); // calls re-render of each child
}
pass the corresponding values to each child as their props in the render method:
render() {
return (
<View>
<ToggleButton onPressFromParent={this.onButtonPress} dataFromParent={this.state.buttonOne} />
<ToggleButton onPressFromParent={this.onButtonPress} dataFromParent={this.state.buttonTwo} />
...
finally each child can use the props:
...
<TouchableWithoutFeedback onPress={() => this.props.onPressFromParent(this.props.dataFromParent.id)}>
<View style={[style.button, this.props.dataFromParent.selected ? style.buttonSelected : style.buttonDeselected]}>
...
I left the title field intentionally for you to try and implement.
P.S: You should be able to follow the code as these are just JS or JSX.
I hope this helps :)
Because children do not rerender if the props of the parent change, but if its STATE changes :)
Update child to have attribute 'key' equal to "selected" (example based on reactjs tho')
Child {
render() {
return <div key={this.props.selected}></div>
}
}
I have an array of items stored in this.state.items and user can add/remove items by clicking buttons which modify this.state.items.
I have something like this. (This code is untested, and may not compile, but you probably get the idea.)
TextField = React.createClass({
render() {
return <input type="text"/>;
}
});
TextList = React.createClass({
getInitialState () {
return {
items: [<TextField />, <TextField />, <TextField />]
};
},
addItem() {
// Adds a new <TextField /> to this.state.items
},
removeItem(index) {
// Filters out the item with the specified index and updates the items array.
this.setState({items: this.state.items.filter((_, i) => i !== index)});
},
render() {
return (
<ul>
{this.state.items.map((item, index) => {
return (
<li key={index}>
{item}
<button onClick={this.props.removeItem.bind(null, index)}>Remove</button>
</li>
);
})}
</ul>
<button onClick={this.addItem}>Add New Item</button>
);
}
});
This can remove the specified item in the this.state.items. I saw it in console, and this part is working properly. But that's not what is presented to the user.
For example, if there are 3 input fields, and user types "One", "Two", and "Three" respectively, then if he clicks on the remove button for "Two", the input field with "Three" is removed instead. In other words, always the last field is removed.
How can I fix this so that the value of the input fields are properly associated with the removed ones?
This is because react recycles items based on their key, for speed and efficiency. Using an index that always is 0,1,2 etc therefore has unwanted consequences.
How react works:
you have a list of items indexed 0,1,2: which react renders.
user deletes first item
the list is now 2 items long: index 0,1
when react re-renders, it deducts (falsely) from your keys that item 0,1 are unchanged (because the keys are identical), and that the third item is removed.
Solution: make the key unique to the specific item. Better to base on item content.