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:
JS Fiddle of example
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?
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} />
}
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
Related
I am attempting to place an image (using absolute positioning) above the fold on the initial render of a page I am building using React (Gatsby SSR). The issue I am having is that the useWindowSize hook fires immediately and then erroneously places the image in the wrong position.
My current solution determines whether the component exists in the vDOM and then uses a setTimeout , before pushing the new position values into state.
// On initial render, position hero container background
useEffect(() => {
if (elementRef && inView) {
setTimeout(() => {
console.log("FIRED");
const boundingClientRect = elementRef.current.getBoundingClientRect();
setContainerWidth(boundingClientRect.width);
setContainerHeight(boundingClientRect.height);
setContainerDistanceFromTop(boundingClientRect.top);
setContainerDistanceFromLeft(boundingClientRect.left);
}, 2000)
}
}, [inView]);
Obviously there are many, many flaws with this approach (won't trigger on slow devices) - but I'm struggling to think of the most optimal way to cause a re-render of the image.
Another solution would be to repeatedly check if the state has changed for a period of time (every second for 10 seconds), but this still doesn't feel very optimal.
I am sure there's a far more elegant approach out there, would be grateful if anybody could assist?
Thanks
Well, you can avoid 4 of your useStates using one single useState that contains an object with all your properties. In addition, you should be able to get rid of the setTimeout because using the empty deps ([]) will ensure you that the DOM tree is loaded (hence your element is in the view).
const [properties, setProperties]= useState({});
useEffect(() => {
if(elementRef.current){
const boundingClientRect = elementRef.current.getBoundingClientRect();
setProperties({
width: boundingClientRect.width,
height: boundingClientRect.height,
top: boundingClientRect.top,
left: boundingClientRect.left
})
}
}, []);
It's important to set initially the elementRef as null the avoid React's memoization and setting initially the value as null before rehydration. In that way, you only need to check for the elementRef.current and setting all properties at once using one single useState. After that, you only need to access each property like: properties.width, and so on.
The inView boolean is also unnecessary since the empty deps ([]) will fire your effect once the DOM tree is loaded.
I am not sure if this is an issue of react-leaflet-markercluster, react-leaflet, leaflet, react, or my code.
I have a map with several thousand markers and I am using react-leaflet-markercluster for marker clustering. If I need to update a global state of MapComponent, there is 1-3 seconds delay when this change is reflected.
I created a codesandox with 5000 markers and you can see there 2 use cases with performance issues:
1.) MapComponent is inside react-reflex element, that allows resizing panel and propagates new dimensions (width, height) to MapComponent. If width and height are changed, mapRef.invalidateSize() is called to update map dimensions. Resizing is extremely slow.
2.) If user clicks on Marker, global state selected is updated. It is a list of clicked marker ids. Map calls fitBounds method to focus on clicked marker and also marker icon is changed. There is around 1 second delay.
In my project, if I need to change a MapComponent state, it takes 2-3 seconds in dev mode when changes are reflected and it is just a single rerender of MapComponent and its elements (markers).
I took a look at Chrome performance profile and it seems like most time is spent in internal React methods.
It is possible to fix this by preventing rerendering using memo, which is similar to shouldComponentUpdate, but it makes whole code base too complicated. preferCanvas option doesn't change anything. I am wondering what is a good way to fix these issues.
The main problem I identified in your code is that you re-render the whole set of marker components. If you memoize the generation of those, you achieve a good performance boost; instead of running the .map in JSX, you can store all the components in a const; this way, the .map won't run on every render.
from this
...
<MarkerClusterGroup>
{markers.map((marker, i) => {
...
to something like this
const markerComponents = React.useMemo(() => {
return markers.map((marker) => {
return (
<MarkerContainer .../>
);
});
}, [markers, onMarkerClick]);
return (
<>
<MarkerClusterGroup>{markerComponents}</MarkerClusterGroup>
</>
);
The second refactor I tried is changing the way you select a marker. Instead of determining the selected prop from the selected array for each marker, I put a selected field on every marker object and update it when selecting a marker. Also, I add the position to the onClickHandler args to avoid looking for that in the markers array.
There are some other tweaks I don't explain here so please check my codesandbox version.
https://codesandbox.io/s/dreamy-andras-tfl67?file=/src/App.js
So I want to display a large table, say 1 billion cells.
The component receives remote updates, each addressing one cell.
Each cell is represented via a div
Here's a naive implementation, which won't be practical, because on each update a large array is created and all cells are updated.
const Table = props => {
const [cells, setCells] = useState(new Array[1_000_000_000])
// ... 'useEffect()' receives remote data and 'setCells()' it.
return cells.map(cell => <div id={cell.id}>{cell.text}</div>)
}
How to implement this efficiently so that performance is top notch?
Ideally, I would like that on each update only 1 array element is updated and only 1 table cell in DOM is updated 🤷♂️ Ideally the solution should be O(1) and work great for tables of 10 to 1'000'000'000 cells. Is it possible in React?
By 1'000'000'000 cells I mean a large number of cells that browser can display at once if cells are created and updated with Vanilla JavaScript.
I'm looking not for a library but for a pattern. I want to find out what is the general approach in React for such cases. It is obvious how to do this in Vanilla JavaScript, you just create divs and then access the required div and update its innerText 🤷♂️. But how this case can be modeled in React?
For very long lists, "virtualized list" is the typical approach so that only some of the cells are actually mounted and the rest are temporarily rendered with placeholders. A couple libraries that implement this are:
https://react-window.now.sh/
https://bvaughn.github.io/react-virtualized
If you are looking for strictly O(1) then typical React patterns may not suitable since React cannot partially render a component. In other words, the render method of your Table component will need to iterate over the list in O(n). Virtualization of the list could reduce from O(size of list) to O(size of window).
You may be able to workaround this by maintaining a list of React refs of each mounted cell, then calling an update method on the single cell by index to trigger a re-render of a single cell, but this is likely not recommended usage of React.
Another option could be the Vanilla JS approach inside of a stateful React component. You'll want to implement the relevant lifecycle methods for mounting and un-mounting, but the update would be handled in Vanilla JS.
class Table {
constructor(props) {
super(props);
this.container = React.createRef();
}
componentDidMount() {
// Append 1_000_000_000 cells into this.container.current
// Later, modify this.container.current.item(index) text
}
componentWillUnmount() {
// Perform any cleanup
}
render() {
return (
<div ref={this.container} />
)
}
}
I'm trying to create a stepper form
I store my steps in an array of json with a proprety component ({typeOfComponent, component, key})
It works wells, but:
Everytime i slice my array, like when i move up/down a step or add a new step between two steps.
I lose the states inside my component.
I tried to use memo, i don't understand why it's only when an item position my composent is recreate. Is it possible like a pointer in C to store only his "adress"
the code sandbox exemple =>
https://codesandbox.io/s/infallible-maxwell-zkwbm?file=/src/App.js
In my real projet, the button ADD is a button for chosing the new step type
Is there any solution for manipulates my steps without losing the user data inside ?
Thanks for your help
React is re-mounting the components inside of this every re-render probably due to a variety of reasons. I couldn't get it to work as is, but by lifting the state up from your components, it will work.
You'd likely need to lift the state up anyway because the data isn't where you need it to be to make any use of your form when the user is done with it.
In order to lift the state up, I added the current value to the steps array:
function addNext(step, index) {
componentKey++;
setSteps(prevState => {
let newState = [...prevState];
step = 1;
newState.splice(index + 1, 0, {
stepNumber: step,
component: getStepContent(step, componentKey),
value: getDefaultValue(step),
key: componentKey
});
return newState;
});
}
I also made sure your getStepContent just returned the component rather than a node so you can render it like this:
<step.component
value={step.value}
onChange={handleChange}
data-index={i}
/>
There are definitely a lot of ways to optimize this if you start running into performance issues, of course.
https://codesandbox.io/s/beautiful-river-2jltr?file=/src/App.js
I have a React application using Material UI with a component (which we can call DatePicker) shown below, sneakily changed for demo purposes.
Material UI animates clicks and other interactions with its components. When clicking a radio button that has already been selected, or a "time button" which doesn't change state, this animation is visible above. However, when such a click changes the state, the animation get interrupted.
I can see why this happens from a technical perspective; the DatePicker component calls setMinutes, which is a property passed in from its parent (where the state lives). This is a React.useState variable, which then updates its corresponding minutes variable. Minutes is then passed into DatePicker, which re-renders due to a prop change.
If state lived within DatePicker then this problem shouldn't rear its head; however, DatePicker is one part of a much larger form which dictates the contents of a table in the parent. To generate rows for this table, the parent must have this information.
Below is a sample reconstruction of the parent:
const Parent = () => {
const [minutes, setMinutes] = React.useState(15);
const [radioOption, setRadioOption] = React.useState('Thank You');
// Many other state variables here to hold other filter information
return (<div>
<DatePicker minutes={minutes} setMinutes={setMinutes} radioOption={radioOption} setRadioOption={setRadioOption}/>
</div>);
};
And here a sample reconstruction of DatePicker:
const DatePicker: React.FC<DatePickerProps> = props => {
const {minutes, setMinutes, radioOption, setRadioOption} = props;
return (<div>
<Radios value={radioOption} onChange={val => setRadioOption(val)}/>
<Minutes value={minutes} onChange{val => setMinutes(val)}/>
</div>);
};
I'm not sure what the best practice is in this situation, but I get the distinct feeling that this is not it. Does anyone have any advice? Thanks in advance!
Thank you for your comment, Ryan Cogswell. I did create a code sandbox, and found that the problem was not about React state management as much as what I was doing beyond what I provided in my question.
I was using the withStyles HOC to wrap my component, in a way similar to const StyledDatePicker = withStyles(styles)(DatePicker). I then used that styled element and put properties (minutes, etc) on that.
It turns out that using the unstyled DatePicker resolves this issue. I troubleshooted this further, and found that I had created the "Styled" component within the "render" method of the parent, meaning every time a prop change was pushed up the chain, the parent would re-render and the entire "Styled" component type would be created again (or so I believe). This would break reference integrity, which explains the "drop and recreate" behaviour.
This teaches the valuable lesson of keeping components small and using code sandboxes for troubleshooting. Thanks again!
For anyone interested, here is the Code Sandbox used for testing.