Avoid looping through map if single item change - reactjs

I have a List component as shown bellow. Component renders list of Items and listens for item changes using websocket (updateItems function). Everything works fine except that I noticed that when a single item change my renderItems function loops through all of items.
Sometimes I have more than 150 items with 30 updates in a second. When this happens my application noticeable slows down (150x30=4500 loops) and when another updateItems happens after, its still processing first updateItems. I implemented shouldComponentUpdate in Items component where I compare nextProps.item with this.props.item to avoid unnecessary render calls for items that are not changed. Render function is not called but looks like that just call to items.map((item, index) slowing down everything.
My question is, is there a way to avoid looping through all items and change only the one that updated?
Note that other object data are not changed in this case, only items array within object.
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
object: null, // containing items array with some other data
// such as objectId, ...
};
}
componentDidMount() {
// call to server to retrieve object (response)
this.setState({object: response})
}
renderItems= (items) => {
return items.map((item, index) => {
return (
<Item key={item.id} item={item}/>
);
});
}
// this is called as a websocket onmessage callback
// data contains change item that should be replaced in items array
updateItems = data => {
// cloning object here in order to avoid mutation of its state
// the object does not contains functions and null values and cloning
// this way works in my case
let cloneObject = JSON.parse(JSON.stringify(this.state.object));
let index = // call to a function to get index needed
cloneObject.items[index] = data.change;
this.setState({object: cloneObject});
}
render() {
return (
this.state.object && {this.renderItems(this.state.object.items)}
);
}
}

First I would verify that your Item components are not re-rendering with a console.log(). I realize you have written that they don't in your description but I'm unconvinced the map loop is the total cause of the issue. It would be great if you posted your Component code because I'm curious if your render method is expensive for some reason as well.
The method you are currently using to clone your last state is a deep clone, it's not only slow but it will also cause each shallow prop compare to resolve true every time. (ie: lastProps !== newProps will always resolve true when using JSON.parse/stringify method)
To keep each item's data instance you can do something like this in your state update:
const index = state.items.findIndex(item => item._id === newItem._id);
const items = [
...state.items.slice(0, index),
newItem,
...state.items.slice(index + 1),
];
Doing this keeps all the other items intact, except for the one being updated.
Finally as per your question how to prevent this list re-rendering, this is possible.
I would do this by using moving the data storage out of state and into two redux reducers. Use one array reducer to track the _id of each item and an object reducer to track the actual item data.
Array structure:
['itemID', 'itemID'...]
Object structure:
{
[itemID]: {itemData},
[itemID]: {itemData},
...
}
Use the _id array to render the items, this will only re-render when the array of _ids is changed.
class List() {
...
render() {
return this.props.itemIds.map(_id => <Item id={_id} />);
}
}
Then use another container or better yet useSelector to have each item fetch its data from the state and re-render when it's data is changed.
function Item(props) {
const {id} = props;
const data = useSelector(state => state.items[id]);
...
}

You can try wrapping the child component with React.memo(). I had a similar problem with a huge form (over 50 controlled inputs). Every time I would've typed in an input all the form would've get re-rendered.
const Item = memo(
({ handleChange, value }) => {
return (
<>
<input name={el} onChange={handleChange} defaultValue={value} />
</>
);
},
(prevProps, nextProps) => {
return nextProps.values === prevProps.values;
}
Also, if you're passing through props a handler function as I did above, it's worth mentioning that you should wrap it inside a useCallback() hook to prevent recreation if the arguments to the function did not changed. Something like this:
const handleChange = useCallback(e => {
const { name, value } = e.target;
setValues(prevProps => {
const newProps = { ...prevProps, [name]: value };
return newProps;
});
}, []);

For your scenario I would recommend don't use state for your array rather create state for every individual element and update that accordingly. Something like this
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
individualObject: {}
};
}
object = response; // your data
renderItems= (items) => {
this.setState({
individualObject: {...this.state.individualObject, ...{[item.id]: item}
})
return items.map((item, index) => {
return (
<Item key={item.id} item={item}/>
);
});
}
updateItems = data => {
let cloneObject = {...this.object}
let index = // call to a function to get index needed
cloneObject.items[index] = data.change;
this.setState({
individualObject: {...this.state.individualObject, ...{[item.index]: item}
})
}
render() {
return (
this.renderItems(this.object)
);
}
}

Related

Cant use filter function in the return statement for reactjs. Throwing error

I'm trying to filter the data directly under the return statement. I am getting this error "Objects are not valid as a React child. If you meant to render a collection of children, use an array instead". Map function works just fine. Map and Filter both return array
Here's my code
export class TestPage extends Component {
constructor(){
super();
this.state = {
proPlayerData: []
}
}
componentDidMount(){
this.fetchData();
this.filterData();
}
filterData = () => {
}
fetchData = async() => {
const playerData = await fetch("https://api.opendota.com/api/playersByRank");
const player_data = await playerData.json()
console.log("fetch",player_data);
await this.setState({proPlayerData: [...player_data]})
}
render() {
// let topTenIds = this.state.proPlayerData
// console.log(topTenIds)
return (
<div>
{this.state.proPlayerData.filter((data,index) => {
if(index <= 10){
return <div key={index}>data.accountId</div>
}
})}
</div>
)
}
}
export default TestPage
Why can't I use filter just like map?
Array.prototype.map transforms data from one format to another, its used in react a lot to transform your arrays of data into JSX
Array.prototype.filter will filter data in your data arrays, but not alter the format, therefore if you start with an array of objects, you will end with an array of objects of the same shape (or an empty array if none meet the condition in the callback)
You need a combination of both, first a filter to filter the data you want, then a map to transform your filtered data into JSX, but even still rather than a filter, which will iterate over each element, you only need the first 10, looking at your example, therefore you can use Array.prototype.slice -
this.state.proPlayerData
.slice(0, 10)
.map((data) => (<div key={index}>{data.accountId}</div>))
edit... looks like you maybe want to the first 11, therefore update the slice args to suit...

How to update the props of a component in a list of components

I'm trying to update a prop value of a component in a list of components. Following is an example of it.
I'm developing an app using ReactNative
...
constructor(props) {
state = {
components: [*list of components*],
}
componentDidMount() {
fetchingAPI().then(response => {
const components = [];
for (const data of response.data) {
components.push(<MyComponent numOfLike={data.numOfLike} />);
}
this.setState({components});
});
}
render() {
return (
...
{this.state.components}
...
);
}
When I want to update a component, I update the whole state named components like :
updateAComponent(index, newNumOfLike) {
const components = this.state.components;
components[index] = <MyComponent numOfLike={newNumOfLike} />
this.setState({components});
}
But, this method change the component, not update. right? I means the components state is updated but MyComponent in components[index] is changed.
So, if I want to update the MyComponent in components[index] using the way of update the props numOfLike directly, how can I do it?
addition :
What I did not mention is that the MyComponent has a Image tag in it. So if I use FlatList or array.prototype.map there are several issues.
If I update the state, the whole list will be re-rendered. So if there are many list item, the speed of updating is very slow.
Since there are Image tag in the list, if I update a list item, the whole Image tags blink since the list items are re-rendered.
In this situation
Is there way to re-render(update) only a component which I want to update? (target updating)
If there in no way to target updating, just let the whole list items(components) re-rendered when just a component is updated?
You can use setNativeProps, described in the direct manipulation documentation
components[index].setNativeProps(propsObj)
You can modify your componentDidMount function like this (so that there are no race around or async conditions in the code) -:
componentDidMount() {
fetchingAPI().then(response => {
this.setState({
components: this.state.components.concat(
response.data.map(i => <MyComponent numOfLike={i.numOfLike} />)
)});
});
}
Can you try with the FlatList?
eg:
...
constructor(props) {
state = {
componentsData: [],
}
componentDidMount() {
fetchingAPI().then(response => {
this.setState({componentsData: response.data});
});
}
_renderItems = ({ item, index }) => {
return(
<MyComponent numOfLike={item. numOfLike} />
)
}
render() {
return (
...
<FlatList
data={this.state.componentsData}
renderItem={this._renderItems}
keyExtractor={(item, index) => index.toString()}
extraData={this.state}
/>
...
);
}
Then when you want to update the list,
updateAComponent(index, newNumOfLike) {
const data = this.state.componentsData;
data[index].numOfLike = newNumOfLike
this.setState({componentsData: data});
}

adding row to a table in react, row is added, but data is not

I have a table of about 500 items, all I'm trying to do is push data to the this.state.data and re-render the table. What's happening is that the row is added, but the data is not shown in the row. If I do a this.forceUpdate(), after a short time, then the data magically appears in the row. I'm assuming my re-render is occurring before the state is updated, how do I get around this? Here's the code that's adding to this.state.data and re-rendering:
// the format we expect
scrubData: function(rawData, cb) {
const scrubbedData = rawData.map((object, idx) => {
let { mediaserver = [] } = object.Relations;
let { latitude = [0], longitude = [0] } = object.Properties;
return {
label: object.Label || '',
name: object.Name || '',
mediaserver: mediaserver[0] || '',
geo: `${latitude[0]}, ${longitude[0]}`,
status: '',
event: '',
}
});
if (cb) {
cb(scrubbedData);
return;
}
return scrubbedData;
},
// push one item to the data array, so we don't have to call
// the entire data set all over again
addData: function(rawData) {
const scrubbedData = this.scrubData([rawData], (scrubbedData) => {
this.state.data.unshift(scrubbedData);
this.setState({
data: this.state.data,
});
});
},
It's because you are using unshift. With react, you should always mutate state by providing updated state, not by changing the current state.
Instead of using unshift, you can use something like concat, or the spread operator.
Using concat:
this.state.data.concat([scrubbedData]);
this.setState({
data: this.state.data,
});
Or using the spread operator:
this.setState({
data: [...this.state.data, scrubbedData]
});
I recommend checking out this stackoverflow post which lists all the mutating methods for arrays.
React works internally when doing the state diff by comparing references to objects/arrays, and since you are mutating the array, the reference is still the same, so React does not detect that a change has been made.
Herein lies the benefit of using an immutable library with React, as changes will always produce a copy, so you can remove an entire class of bugs such as this.
EDIT:
You are calling this.scrubData before adding the new row, and the result is that the new row has none of the additional data that you want appended to it. Try adding the new row to the array first, and then calling that function to append data to each row.
Okay, so I finally got this to work. I had to use componentWillUpdate. Not sure if this is correct, but it works now.
export default class Table extends React.Component {
constructor(props) {
super(props);
this.state = {
tableHeight: "50vh",
data: [],
}
}
componentWillUpdate(nextProps, nextState) {
this.state.data = nextProps.data;
}
render() {
const tableRow = this.state.data.map((object, idx) => {
return (
<TableRow key={idx} data={object} />
)
})
return (
<div className="table">
<tbody>
{tableRow}
</tbody>
</table>
</div>
</div>
)
}
}

Hold the component's rendering until the store has finished hydration

I'm fetching my initial data like so:
export function populate (dispatch) {
let posts = []
dispatch(requestNews())
fetch(someEndPoint)
.then(payload => payload.json())
.then(items => {
//some fetching logic that populates posts list above
})
.then(() => { dispatch(receiveNews(posts)) })
.catch(err => {
console.log(err)
})
}
function request () {
return {
type: REQUEST_NEWS,
payload: {
populating: true
}
}
}
function receive (posts) {
return {
type: RECEIVE_NEWS,
payload: {
populating: false,
posts
}
}
}
As you can see above I'm setting the store with a field called populating which starts as false and changes to true when the 'request' is dispatched and then back to false when 'received' is dispatched.
Then my component looks something like the following:
import { populateNews } from '../modules/news'
class News extends React.Component {
constructor (props) {
super(props)
//this.mapPosts = this.mapPosts.bind(this)
}
componentWillMount () {
populateNews(this.props.dispatch)
}
render () {
if (!this.props.news.populating) {
return (
<div>
{this.props.news.posts[0].title}
</div>
)
} else {
return (
<div>loading</div>
)
}
}
}
On initial load render is being called before the store is populated with the fetched posts even though my populate switch changes back and forth as expected.
I've tried dealing with it using a local state on the component, so it's constructor has: this.state = {populating: false} and then the action creator changes that, but got the same result.
So at the moment my solution is to instead check if the state has a slice called 'posts' which is being created after the content is fetched and if it does to render it. like so:
render () {
return (
<div>
{this.props.news.posts ? <div>{this.props.news.posts[0].title}</div> : null }
</div>
)
}
This of course just renders the component and then renders it again after the store is updated with the posts, and is not an optimal solution like waiting with the render until the fetch is completed and the store is populated.
There's a long discussion about it here:
https://github.com/reactjs/react-redux/issues/210
How can I delay or better put condition the render itself?
Hmm, you might want to change your if statement. Unless your store is initializing populating to true, it will be undefined on the initial load and will pass your if (!this.props.news.populating) validation which will try to render the post title. Change your condition to look for a truthy value rather than a falsey value and you should have more control over it.

How to know if all the setState updates have been applied to the state in a React component?

I was reading the documentation about React setState, which says:
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.
Now I have a component like this:
class NoteScreenComponent extends React.Component {
constructor() {
super();
this.state = { note: Note.newNote() }
}
componentWillMount() {
this.setState({ note: this.props.note });
}
noteComponent_change = (propName, propValue) => {
this.setState((prevState, props) => {
let note = Object.assign({}, prevState.note);
note[propName] = propValue;
return { note: note }
});
}
title_changeText = (text) => {
this.noteComponent_change('title', text);
}
body_changeText = (text) => {
this.noteComponent_change('body', text);
}
saveNoteButton_press = () => {
// Save note to SQL database
Note.save(this.state.note)
}
render() {
return (
<View>
<TextInput value={this.state.note.title} onChangeText={this.title_changeText} />
<TextInput value={this.state.note.body} onChangeText={this.body_changeText} />
<Button title="Save note" onPress={this.saveNoteButton_press} />
</View>
);
}
}
What I'm wondering is, since setState does not update the state immediately, how can I know if the note I'm saving in saveNoteButton_press is the current version of the state? Is there some callback or something that I could poll to know if state has been fully updated?
What they are warning against is trying to do something in the same event loop.
method = () => {
this.setState({ note: 'A' })
saveNote(this.state.note) // <-- this.state.note will not have been updated yet.
}
or to setState using previous state:
method = () => {
let note = this.state.note // possible that `this.state.note` is scheduled to change
this.setState({ note: note + 'B' })
}
Since your user is going to be pushing the button after the setState scheduling, the state will have already been updated.
..but for theory's sake, let's imagine that somehow the input event and button happen in the exact same moment.. what would be the correct solution? If it was a single function call you probably wouldn't be using the new state since you already have the new note and the previous state.
method = (text) => {
let noteToSave = this.state.note + text // old state + new value
saveNote(noteToSave) // maybe this will fail
.then(response => this.setState({ note: noteToSave }))
.catch(err => this.setState({ error: 'something went wrong' }))
// optimistically update the ui
this.setState({ note: noteToSave })
}
but probably the most likely solution is to just pass what you want as an argument where you use it, rather than trying to access state which might be in a race condition, since render will happen after any state transitions.
method = (note) => {
noteToSave(note)
}
render() {
return (
<Button onPress={() => this.method(this.state.note)} /> <-- must be up to date here
)
}

Resources