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 !
Related
I'm trying to avoid re-rendering unnecessary components
For example:
const[ValueState,SetValueState]=useState(5); //hook
<View>
<Text>{ValueState}</Text>
<View>
{
[...Array(3)].map((index,el)=>{
return (<View><Text>Hello there</Text></View>)
})
}
</View>
</View>
here every time I change the value of ValueState the entire map() segment also gets re-rendered
how do I avoid this and make the map() segment only render 1 time?
It depends on what the function you don't want to re-render depends on. In this case, your array and map function do not depend directly to ValueState.
One way to achieve 1 re-render is using React.memo
Example to render map function only once
import React, { useState } from "react";
import { View, Text, TouchableOpacity } from "react-native";
const ArrayMapSection = React.memo(()=> {
console.log("ArrayMapSection rendered")
return [...Array(3)].map((index,el)=>{
return (<View><Text>Hello there</Text></View>)
});
})
const App = () => {
const [ValueState,SetValueState]=useState(5); //hook
return(
<View>
<Text>{ValueState}</Text>
<TouchableOpacity onPress={()=>SetValueState(Math.random())}>Press to state update</TouchableOpacity>
<View>
<ArrayMapSection />
</View>
</View>
)
};
export default App;
If you run this program, you would see ArrayMapSection renderedonly once in the console. Now try to change the ValueState by pressing on Press to state update. The ArrayMapSection won't re-render because React.memo only re-renders if props changes
More info: https://reactjs.org/docs/react-api.html#reactmemo
Create a custom react component that takes the array as a prop and maps it to other components.
That way the component is only rerenderd if the array prop changes.
Code example:
const[ValueState,SetValueState]=useState(5);
<View>
<Text>{ValueState}</Text>
<CustomList array={[1,2,3]} />
</View>
export const CustomList = (array) => {
return (
<>
{
array.map((index,el)=>{
return (<View><Text>Hello there</Text></View>)
})
}
<\>
)
}
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 can see item and item id as it loops to render on the screen but i don't see the value of id when i click on any of the Tile where Tile is a div and react styled component.
class CategoryOffers extends React.Component {
passidtopointscreen =(id)=>{
console.log("id is", id);
localStorage.setItem('points_id',id);
this.props.history.push('/marketplacepoints')
debugger
}
render() {
debugger
return (
<Wrapper>
{this.props &&
this.props.cards_data &&
this.props.cards_data.map(item => {
return (
<Tile onClick={(item)=>this.passidtopointscreen(item.id)}>
<ImageWrapper>
<Image src={item.logo} height={'24px'} width={'73px'} />
</ImageWrapper>
<CardString>{item.offer_summary}</CardString>
</Tile>
)
})}
</Wrapper>
)
}
}
onClick={()=>this.passidtopointscreen(item.id)}
while adding item there you create new instance for this keyword for no reason
By having the same argument-name as your already decleared argument (item), you overwrite the outer argument. There should be no reason for you here to use the event-argument, if I have understood your question correctly.
I would also suggest avoiding localstorage and instead make use of the state.
I made the component into functional one here:
import React from "react";
const CategoryOffers = ({history,cards_data}) => {
const passidtopointscreen =(id)=>{
localStorage.setItem('points_id',id);
history.push('/marketplacepoints')
}
return (
<Wrapper>
{
cards_data?.map(item => {
return (
<Tile onClick={(event)=>passidtopointscreen(item.id)}>
<ImageWrapper>
<Image src={item.logo} height={'24px'} width={'73px'} />
</ImageWrapper>
<CardString>{item.offer_summary}</CardString>
</Tile>
)
})}
</Wrapper>
)
}
}
I'm very new to react native and I'm trying to update list dynamically.
Below is my code:
import React, { Component } from "react";
import { View, Text, StyleSheet, FlatList } from "react-native";
import { Tile, ListItem, List } from "react-native-elements";
export default class JoinSession extends Component {
constructor() {
super();
this.state = {
dataToRender: [{ "id": "0", "name": "name0", "des": "des0" }]
}
}
componentDidMount() {
var counter = 0;
const interval = setInterval(() => {
try {
var temp = {
"id": ++counter + "",
"name": "name" + counter,
"des": "des" + counter
}
let tempArray = this.state.dataToRender;
tempArray.push(temp);
this.setState({
dataToRender: tempArray
});
console.log(this.state.dataToRender);
if(counter === 10) {
clearInterval(interval);
}
} catch (e) {
console.log(e.message);
}
}, 2000);
}
renderList(item) {
console.log(item);
return (
<ListItem
roundAvatar
title={item.name}
subtitle={item.des}
/>
);
}
render() {
return (
<View style={{ flex: 1, alignItems: "stretch", backgroundColor: "skyblue" }}>
<List>
<FlatList
data={this.state.dataToRender}
renderItem={({ item }) => this.renderList(item)}
keyExtractor={item => item.id}
/>
</List>
</View>
);
}
}
I am only able to get first element which I've declared in the constructor but data which I'm appending in serInterval is not showing up on the page.
Please help me to resolve it or if there is any other way to do it, please let me know.
Thanks in advance.
Hi try to have a look on the extraData parameter you can use on a FlatList:
By passing extraData={this.state} to FlatList we make sure FlatList itself will re-render when the state.selected changes. Without setting this prop, FlatList would not know it needs to re-render any items because it is also a PureComponent and the prop comparison will not show any changes.
<FlatList
...
extraData={this.state}
/>
More info on the FlatList documentation: https://facebook.github.io/react-native/docs/flatlist.html
Also you shouldn't need this <List> from react native element here the list behaviour is totally handle by your FlatList.
Like say #AlexDG Flat list is Pure component. For updating pure component use key prop.
But do not overdo it, otherwise you can get unnecessary redrawing.
<FlatList
key={this.state.dataToRender.length} <---------- rerender
data={this.state.dataToRender}
renderItem={({ item }) => this.renderList(item)}
keyExtractor={item => item.id}
/>
I just had this on my own and just so happened to read this comment by OP:
The problem was array mutation. Never mutate array or object in react native.
And I indeed changed my state this way:
this.setState(prev =>
prev.listData.push("stuff");
return prev;
});
And you can see it in the question as well:
let tempArray = this.state.dataToRender;
tempArray.push(temp);
After changing it to
this.setState(prev => {
let copy = Array.from(prev.listData);
copy.push("stuff");
return {listData: copy};
});
however, my list was updating just fine!
So if you are mutating an array in your state that is related to your list's data you might want to see if this helps some.
I know there are many answers out there for this issue but I couldn't find one that exactly solved my problem. I am getting the following error : Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of QuestionItem. See https://fb.me/react-warning-keys for more information.
I am setting a key for the component but I can't get the warning to go away.
Main component :
renderData() {
return this.state.data.map((data) => {
return (
<QuestionItem key={data._id} data={data} delete={this.deleteItem} edit={this.editItem} />
)
})
}
QuestionItem component :
import React, { Component, PropTypes } from 'react';
import Card from 'material-ui/lib/card/card';
import CardActions from 'material-ui/lib/card/card-actions';
import CardHeader from 'material-ui/lib/card/card-header';
import CardMedia from 'material-ui/lib/card/card-media';
import CardTitle from 'material-ui/lib/card/card-title';
import FlatButton from 'material-ui/lib/flat-button';
import CardText from 'material-ui/lib/card/card-text';
import Delete from 'material-ui/lib/svg-icons/action/delete';
import ModeEdit from 'material-ui/lib/svg-icons/editor/mode-edit';
import IconButton from 'material-ui/lib/icon-button';
import Button from '../UI/Button';
class QuestionItem extends Component {
renderTags() {
return this.props.data.tag.map((tag) => {
return (
<FlatButton label={tag} />
)
})
}
renderCompany() {
return this.props.data.company.map((company) => {
return (
<FlatButton label={company} />
)
})
}
edit = () => {
this.props.edit(this.props.data);
}
delete = () => {
this.props.delete(this.props.data._id);
console.log(this.props.data._id);
}
render() {
return (
<Card style={{margin: 50}}>
<CardTitle title={this.props.data.text} />
<CardText>
{this.props.data.answer}
</CardText>
<CardActions>
{ this.renderTags() }
{ this.renderCompany() }
<IconButton onClick={this.delete} style={{float: 'right'}}>
<Delete />
</IconButton>
<IconButton onClick={this.edit} style={{float: 'right'}}>
<ModeEdit />
</IconButton>
</CardActions>
</Card>
)
}
}
export default QuestionItem;
What am I missing here?
Well you'll need to log out the data._id and verify that they are all unique. Or you can do this:
renderData() {
return this.state.data.map((data, index) => {
return (
<QuestionItem key={index} data={data} delete={this.deleteItem} edit-{this.editItem} />
);
});
}
As the other answer pointed out, the other calls to map that go to a render need to set the key prop too to a unique value.
So these:
renderTags() {
return this.props.data.tag.map((tag) => {
return (
<FlatButton label={tag} />
)
})
}
renderCompany() {
return this.props.data.company.map((company) => {
return (
<FlatButton label={company} />
)
})
}
Should become:
renderTags() {
return this.props.data.tag.map((tag, index) => {
return (
<FlatButton key={index} label={tag} />
);
});
}
renderCompany() {
return this.props.data.company.map((company, index) => {
return (
<FlatButton key={index} label={company} />
);
});
}
Note we are using index which is the array index. It is basically like a synthetic identifier in SQL. If what you're actually rendering has unique identifiers already, it is better to use those! For example, the key prop for a tag could be just the tag -- the string itself. The key prop supports multiple types:
react - nodes-and-elements:
key : string | boolean | number | null,
So if your tags are unique (I would expect them to be but obviously don't want to assume), you could do this:
renderTags() {
return this.props.data.tag.map((tag) => {
return (
<FlatButton key={tag} label={tag} />
);
});
}
You might consider doing instead something like (tag || '').toLowerCase().replace(' ', '_') however I think React is already doing some manipulation there (besides potentially character case). So just passing the tag itself should be good! You can inspect the DOM to see data-reactid if you're not running a version that got rid of it (I think 0.15 gets rid of it). The React developer tools might let you inspect the key with 0.15.
Update
I do not recommend using the array index as the key. It causes subtle bugs. To see this in action, make an array of objects, render them using the array index and then mutate the array by removing say the 2nd element (and ensure React renders again). Now the indexes don't correspond to the same objects. My recommendation is to always set a key to a unique value. During development, it might be best not to set a key until you find one rather than using the array index because then the errors on the console will remind you to fix this before deploying/committing your change.
In renderTags() and renderCompany() you have iterators with no keys.