Reputation: 3
I'm a beginner when it comes to react.js and I'm building a component that contains a large number of changing items.
TLDR:
I have a Parent component that contains many Child components (think > 1000) that change their state very quickly. However the state of the child components needs to be known in the parent component - therefore I lifted the state of all children to the parent component. Since all child components are rendered every time the state in the parent changes, the performance is pretty bad. A single changing pixel can take more than 200ms to update. Implementing shouldComponentUpdate
on the Child component is still too slow. Do you have general advice how to handle such a case?
As a specific example of my issue I created an "graphics editor" example with a PixelGrid
component consisting of 32 by 32 Pixel
components:
When the onMouseDown
or onMouseEnter
event is called on the Pixel
component, the event is passed up to the parent PixelGrid
component through prop callbacks, and the corresponding state (PixelGrid.state.pixels[i].color
) is changed. Keep in mind that the PixelGrid component is supposed to be able to access all pixel values for further functionality, so keeping state in Pixel
itself is really not an option, I think. But this means, that the whole PixelGrid
component needs to be re-rendered when a single pixel changes. This is obviously very slow. I implemented shouldComponentUpdate
on the Pixel
component to speed things up a little, but this is still not very fast, since every Pixel
is tested for changes.
My first reaction was to manually change the pixel's inline CSS in the DOM through React refs and not keep the pixel state in this.state.pixels, but in this.pixels, so a state change doesn't cause re-rendering, but it seems like a bad to maintain the visual representation "manually".
So, how would you implement such a functionality with React?
Upvotes: 0
Views: 1983
Reputation: 111
You're right to raise state to the parent so that it can control the data. Your Fiddle cannot be optimised because you are mutating state on line 82.
setPixelColor(row, col, color){
const {pixels} = this.state;
pixels[row * this.props.cols + col].color = color; // mutates state
this.setState({pixels: pixels});
}
When you run this.state.pixels[n].color = color
, you're re-assigning a (nested) property on an item in the array in state. This is a form of mutation.
To avoid this, you can spread a copy of the state into a new variable and mutate that:
setPixelColor(row, col, color){
const newPixels = [...this.state.pixels]; // makes a fresh copy
newPixels[row * this.props.cols + col].color = color; // mutate at your leisure
this.setState({pixels: newPixels});
}
Implemented properly, it should not concern you whether "the whole PixelGrid component needs to be re-rendered when a single pixel changes." This is what needs to happen for your pixels to change colour. React's diffing algorithm is designed to update the minimum no. of elements as necessary. In your case, React will update only the style property on the relevant elements. (See React docs: Reconcilliation)
However, React cannot diff properly if it's not sure exactly what has changed, e.g if elements do not have unique keys, or if the element type changes (e.g. from <div>
to <p>
).
In your Fiddle, you map over the array, then calculate index for each one, and set that as they key.
Even though you are not re-ordering the elements in your Fiddle example, you use let
instead of const
, and mention that this is not the whole code. If your real code employs index
, or the current row
or column
, as a key, but the order changes, then React will unmount and remount every child. That would certainly affect your performance.
(You don't need to calculate the index, by the way, as it is available as the second param in .map
, see MDN).
I'd recommend adding a unique id
property to each initialised pixel object, which is set in the parent, and does not change. As you're not using data with uuids, perhaps their inital position:
pixels.push({
col: col,
row: row,
color: 'white',
id: `row${row}-col${col}`; // only set once
});
I've made a quick fiddle with the above changes in one to demonstrate that this works: https://jsfiddle.net/bethylogism/18ezpoqc/4/
Again, the React docs on Reconcilliation are very good, I highly recommend for anyone trying to optimise for whom memoisation is not working: https://reactjs.org/docs/reconciliation.html
Upvotes: 1
Reputation: 670
Use React.memo
to prevent the child components from rendering when the parent renders but the child props don't change.
Example (random guess at what your Pixel
component looks like):
const Pixel = React.memo(({x, y, color, ...rest}) =>
<div style={{
width: 1,
height: 1,
x,
y,
backgroundColor: color
}}
{...rest}
/>)
Now keep in mind if you are passing functions into Pixel
they also need to be memoized. For instance, doing this is incorrect:
const Parent = () => {
// the callback gets redefined whenever Parent rerenders, causing the React.memo to still update
return <Pixel onClick={() => {}} />
}
instead you would need to do
const Parent = () => {
const cb = useCallback(() => {}, []);
return <Pixel onClick={cb} />
}
Upvotes: 1