I have the following use-case:
- Component A is a memoized (
React.memo) component whose ref is being forwarded (React.forwardRef). - Component B is a memoized component (
React.memo) that renders multiple A components, by mapping over a list.
When component B's list gets a new element, all children A components are re-rendered, when they shouldn't.
Code
type Handle = {
jump: () => void;
};
const Y = React.memo(
React.forwardRef<Handle, { entry: number }>((props, ref) => {
console.log("render");
React.useImperativeHandle(
ref,
() => ({
jump: () => {
console.log("test");
},
}),
[]
);
return <div>{props.entry}</div>;
})
);
const X: React.FC = React.memo(() => {
const nodes = React.useRef<{ [key: string]: Handle }>({});
const [array, setArray] = React.useState([1, 2, 3]);
const handleClick = React.useCallback(() => {
setArray(array.concat(4));
}, [array]);
const jumpToNode = React.useCallback(() => {
nodes.current["1"]?.jump();
}, []);
return (
<>
<button onClick={handleClick}>add new</button>
<button onClick={jumpToNode}>jump to first node</button>
{array.map((e) => (
<Y
ref={(no: Handle) => { // <---- CULPRIT
nodes.current[e.toString()] = no;
}}
entry={e}
key={e}
/>
))}
</>
);
});
Investigation
When commenting out the ref that's being passed down to each component A, only the new incoming node gets rendered, while all other nodes do not re-render.
I created a simple codesandbox example in https://codesandbox.io/p/sandbox/react-typescript-forked-jckhqq?file=%2Fsrc%2FApp.tsx, that showcases the issue mentioned above. With the console open:
- All nodes will initially be rendered once
- Click the "add new" button
- You will see only a single "render" console, coming from the new node
If the ref is then commented in, every time the "add new" button is clicked, all nodes are re-rendered.
I don't understand why that is happening and what one can do to avoid these unnecessary re-renderings. Our specific use-case is a computationally expensive component and every single render counts.
React version: 16.9.0
The
refcausesReact.memoto considerpropsas changed, and the component is re-rendered. Thememoaccepts a custom comparison function (arePropsEqual) to check if the props have changed as a 2nd parameter, and you can use it to ignore certain parameters when checking for equality. However, althoughReact.memodoesn't ignore theref, therefis compared regardless of the result ofarePropsEqual.So the simple solution, in my opinion, is to avoid using
refForwardingin this case, passing therefas a normal parameter (jumpRefin the example), and ignoring it in theReact.memo(sandbox):