In React I do often need to use something like useCallback
to remember a function in a list of items (created via a loop) to avoid single element occurrences due to reference identifier mismatches Change all components without re-rendering... Unfortunately, this is surprisingly hard to expire. For example, consider the following 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 ant design, etc. This function reference will then be different on each render because it is inline, thus forcing a re-render.
To avoid this problem, I can think of another solution: create a new component MyButton
, which accepts two properties index={i}
and onClick
instead of a single onClick
and append the parameter 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}> <> }) } )
While this does work, it is very impractical for a number of reasons:
Button
and rewrite components that were not originally intended to handle this nesting... This would break modularity and make the code more complex<MyButton index1= {index1} index2={index2} index3={index3 onClick={myFunction}>
, which means I need to create a more complex version MyButton
completely generic to check for nested levels quantity. I can't use index={[index1,index2,index3]}
because this is an array and therefore has no stable reference. index
es, which means it is more difficult to share code or develop libraries between projectsAm I missing a better solution? Considering lists are everywhere, I can't believe there isn't a proper solution for this, and I'm surprised to see how little documentation there is on this.
edit I try to do this:
// 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 use it like this:
const myactionIndexed = useCallback((e, i) => dispatch(removeSolverConstraint({id: i})), []); return <WrapperOnClick index={i} Component={Button} onClick={myactionIndexed} danger><CloseCircleOutlined /></WrapperOnClick>But it's still not perfect, in particular I need a wrapper for different nesting levels and I need to create a new version every time I target a new property (
onClick
, onChange
,...), it doesn't work directly if I have multiple properties (e.g. onClick
and onChange
), I've never seen this before, so I guess there Better solution.
edit I tried various ideas, including using fast-memoize, but I still don't understand all the results: sometimes, fast-memoize works, sometimes it fails... and I don't know if fast-memoize is a recommended solution: It seems strange to use a third party tool for such a common use case. Check out my tests here https://codesandbox.io/embed/magical-dawn-67mgxp?fontsize=14&hidenavigation=1&theme=dark
Test here https://codesandbox.io /s/sharp-wind-rd48q4?file=/src/App.js
Warning: I'm not a React expert (hence my question!), so please leave a comment below and/or add a 1 if you think this solution is the canonical way to do it in React (or -1 Not ^^). I'm also curious why some other solutions fail (e.g. based on proxy-memoize (which actually takes 10x longer than no caching, and doesn't cache at all) or fast-memoize (which doesn't always cache, depending on how I use it )), so if you know I'm interested in knowing)
Since I have little interest in this problem, I tried benchmarking a bunch of solutions (14!) against various options (no memory, using external libraries (fast memory vs. proxy memory), using wrappers), Using external components etc...
The best way seems to be to create a new component containing the entire element of the list , not just the last button. This allows for pretty clean code (even if I need to create two components for the list and the item, at least it makes sense semantically), avoids external libraries, and seems to be more efficient than everything else I've tried (at least in my opinion (for example):
I still don't really like this solution because I need to forward a lot of content from the parent component to the child component, but this seems to be the best solution I can get...
You can see a list of my attempts here, I used the code below. Here is the view from the profiler (technically the time difference between all versions is not that big (except version 7 which uses proxy-memoize, I removed it because it was longer, maybe 10x, and was being made Charts are harder to read), but I expect this difference to be greater on longer lists, where the items are more complex to draw (here I only have one text and one button). Note that all versions are not exactly the same (some use
, some use
, some normal lists, some Ant designed lists...), so time to compare It only makes sense between versions that do the same thing. Anyway, my main concern is to see what's cached and what's not cached, which is clearly visible in the profiler (light gray blocks are cached):
Another interesting fact is that you may want to benchmark before memorizing, as the improvement may not be significant, at least for simple components (size 5 here, just one text and one button).