This is a simplified version of my problem
https://codesandbox.io/s/withered-night-6jbgft
import React, { useState, useCallback } from "react";
import "./styles.css";
const Box = React.memo(({ c, i, handleClick }) => {
return (
<div
className="box"
style={{ background: c }}
onClick={() => handleClick(i)}
></div>
);
});
export default function App() {
const COUNT_BOXES = 250;
const [colour, setColour] = useState("#00ff00");
const [boxes, setBoxes] = useState(Array(COUNT_BOXES).fill("#ff00ff"));
const changeColour = useCallback(
(i) => {
setBoxes((prevBoxes) => {
prevBoxes[i] = colour;
return [...prevBoxes];
});
},
[colour]
);
return (
<div className="App">
<input
type="color"
value={colour}
onChange={(e) => setColour(e.target.value)}
/>
Change colour
<div className="wrapper">
{boxes.map((c, i) => (
<Box key={i} c={c} i={i} handleClick={changeColour} />
))}
</div>
</div>
);
}
I have a series of components that can be interacted with (the boxes). When you click a box, it changes colour. The colour it gets changed to is another piece of state in the parent component. The current implementation means that when the colour
state changes, then useCallback
creates a new function for changeColour
, which is passed to every box. So every box rerenders.
How can I refactor this so that every box doesn't need to rerender when the parent colour changes?
CodePudding user response:
One possibility would be to store the parent's changeColour
function inside a ref, so that the reference is stable. It's not the usual React way of doing things, but it works.
const { useState, useCallback, useEffect } = React;
const Box = React.memo(({ c, i, handleClickRef }) => {
console.log('render');
return (
<div
className="box"
style={{ background: c }}
onClick={() => handleClickRef.current(i)}
></div>
);
});
function App() {
console.log('app rendering');
const COUNT_BOXES = 250;
const [colour, setColour] = useState("#00ff00");
const [boxes, setBoxes] = useState(Array(COUNT_BOXES).fill("#ff00ff"));
const changeColourRef = React.useRef();
const changeColour = useCallback(
(i) => {
setBoxes((prevBoxes) => {
prevBoxes[i] = colour;
return [...prevBoxes];
});
},
[colour]
);
useEffect(() => {
changeColourRef.current = changeColour;
}, [colour]);
return (
<div className="App">
<input
type="color"
value={colour}
onChange={(e) => setColour(e.target.value)}
/>
Change colour
<div className="wrapper">
{boxes.map((c, i) => (
<Box key={i} c={c} i={i} handleClickRef={changeColourRef} />
))}
</div>
</div>
);
}
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
.App {
font-family: sans-serif;
text-align: center;
padding: 30px;
}
.wrapper {
display: flex;
flex: 0 0 40px;
flex-wrap: wrap;
}
.box {
cursor: pointer;
width: 20px;
height: 20px;
border: 1px solid black;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>
It would be nicer if you could just let the children re-render - it usually won't be a problem (and if it is a problem, usually it's fixable by tweaking your code, while continuing to let the component re-render).