The problem
In react, I really often need to memoize functions in lists of items (created via loops) using something like useCallback
to avoid re-rendering all components if a single element changes, due to a mismatching reference identifier… Unfortunately, this is surprisingly hard to due. For instance, consider this code:
const MyComp = memo({elements} => {
{
elements.map((elt, i) => {
<>{elt.text}<Button onClick={(e) => dispatch(removeElement({id: i}))}> <>
})
}
})
where Button
is an external component provided by, e.g., ant design. Then, this function reference will be different at every render since it is inlined, hence forcing a re-render.
A (bad) solution
To avoid that issue, I can think of one other solution: create a new component MyButton
, that accepts two props index={i}
and onClick
instead of a single onClick
, and that appends the arguement index
to any call to onClick
:
const MyButton = ({myOnClick, id, ...props}) => {
const myOnClickUnwrap = useCallback(e => myOnClick(e, id), [myOnClick]);
return <Button onClick={myOnClickUnwrap} ...props/>
};
const MyComp = memo({elements} => {
const myOnClick = useCallback((e, id) => dispatch(removeElement({id: id})), []);
return
{
elements.map((elt, i) => {
<>{elt.text}<Button id={i} onClick={myOnClick}> <>
})
}
)
Why I want a better approach
While this does work, this is highly non-practical for many reasons:
- the code is cluttered
- I need to wrap all elements from external libraries like
Button
and rewrite components that were not meant to deal with this kind of nesting… which defeats modularity and complicates the code even more - this composes poorly: if I want to nest elements inside multiple lists, it will be even dirtier as I need to add one new index per level of list like
<MyButton index1={index1} index2={index2} index3={index3} onClick={myFunction}>
, which means that I need in full generality to create an even more complicated version ofMyButton
to check the number of nested level. I can’t useindex={[index1, index2, index3]}
as this is an array and has therefore no stable reference. - as far as I see there is no convention an the naming of
index
es, which means that it is harder to share code between projects or develop libraries
Is there a better solution I am missing? Given how omnipresent lists are, I can’t believe that there is no proper solution for this, and I’m surprised to see very little documentation on this.
Edit
I tried to do:
// Define once:
export const WrapperOnChange = memo(({onChange, index, Component, ...props}) => {
const onChangeWrapped = useCallback(e => onChange(e, index), [onChange, index]);
return <Component {...props} onChange={onChangeWrapped} />
});
export const WrapperOnClick = memo(({onClick, index, Component, ...props}) => {
const onClickWrapped = useCallback(e => onClick(e, index), [onClick, index]);
return <Component {...props} onClick={onClickWrapped} />
});
and to use it like:
const myactionIndexed = useCallback((e, i) => dispatch(removeSolverConstraint({id: i})), []);
return <WrapperOnClick index={i} Component={Button} onClick={myactionIndexed} danger><CloseCircleOutlined /></WrapperOnClick>
but this is still not perfect, in particular I need one wrapper for different nested levels, I need to create a new version whenever I target a new attribute (onClick
, onChange
, …), it would not work directly if I have multiple attributes (e.g. both onClick
and onChange
), and I never saw this before so I guess there is a better solution.
edit
I tried various ideas, including using fast-memoize, but I still don’t understand all the results: sometimes, fast-memoize works, while sometimes it fails… And I don’t know if fast-memoize is the recommended solution: seems weird to use a third-party tool for such a common usecase. See my tests here https://codesandbox.io/embed/magical-dawn-67mgxp?fontsize=14&hidenavigation=1&theme=dark
2
Answers
WARNING: I’m not a react expert (hence my question!), so please comment below and/or add +1 if you think this solution is the canonical way to proceed in React (or -1 of not ^^). I’m also curious to understand why some other solutions failed (e.g. based on proxy-memoize (that is actually 10x longer than without cache, and that does not cache at all) or fast-memoize (that do not always cache depending on how I use it)), so if you know I’m interested to know)
Since I got little interest in this question, I tried to benchmark a bunch of solutions (14!), depending on various choices (no memoization, using on external library (fast-memoize vs proxy-memoize), using a Wrapper, using an external component etc…
The best approach seems to be to create a new component containing the whole element of the list, and not only the final button for instance. This allows a reasonably clean code (even if I need to create two components for the list and for the items, at least it semantically makes sense), avoids external libraries, and seems to be more efficient than all other approaches I tried (at least on my example):
I still don’t incredibly like this solution as I need to forward many stuff from the parent component to the children component, but it seems like it’s the best solution I can get…
You can see my list of attempts here, where I used the code below. And here is the view of the profiler (technically there is not a huge difference in term of time between all versions (except for version 7 that uses proxy-memoize, that I removed because it was wayyyyy longer, maybe 10x and was making the graph harder to read), but I expect this difference to be much larger on longer lists where items are more complicated to draw (here I just have one text and one button). Note that all versions are not exactly the same (some use
<button>
, some</Button>
, some normal lists, some Ant design lists…), so time comparison only make sense between versions that do the same thing. Anyway, I mostly care about seeing what is cached and what is not, which is clearly visible in the profiler (light grey blocks are cached):Another interesting fact is that you might want to benchmark before memoizing, as the improvement might not be significant, at least for simple components (here size 5, only one text and one button).
The test here https://codesandbox.io/s/sharp-wind-rd48q4?file=/src/App.js