React hooks & the closure hell 2
This is a continuation of my last post (React hooks & the closure hell)
Just a quick tl;dr
Functional components require you to regenerate all your callbacks on each re-render because there is nowhere to store them (In old class based components you could just bind your functions as method and you were good to go).
Previous solutions required you to either specify dependencies so they could be passed to existing function, or to work with objects that would store current properties and values. I think these solutions were cumbersome, so I kept tinkering and created even better solution!
Meet useCallbacks
const useCallbacks = (reinit) => {
const data = useRef({ callbacks: {}, handlers: {} })
const callbacks = data.current.callbacks
const handlers = data.current.handlers
// Generate new callbacks
reinit(callbacks)
// Generate new handlers if necessary
for (let callback in callbacks) {
if (!handlers[callback]) {
handlers[callback] = (...args) => callbacks[callback](...args)
}
}
// Return existing handlers
return handlers
}
Usage (Try here)
const App = () => {
const [value, setValue] = useState(1);
const handlers = useCallbacks(callbacks => {
callbacks.handleClick = (event) => {
setValue(value + 1)
}
})
// Check console, the state has changed so the App function will re-run
// but memoized ExpensiveComponent won't be called because the actual handling
// function hasn't changed.
console.log(value)
return (
<div className="app">
<ExpensiveComponent onClick={handlers.handleClick} />
<button onClick={handlers.handleClick}>
I will not trigger expensive re-render
</button>
</div>
);
};
And that's it!
You don't have to specify any dependencies or work with messy objects.
The callback is regenerated but the actual handling function is not, so your pure components or memoized components won't re-render unnecessarily.
Everything works as hooks intended!
Tell me what you think.
Top comments (8)
Your post enlightened me a lot.
I wrote a simpler function based on your idea. On TypeScript, this style may be preferable since different handlers can have different types.
codesandbox.io/s/react-usehandler-...
In this particular situation, if we pass an update function to setValue instead, it suffices to simply use useCallback with an empty dependency list. (Notably,
useCallback((...) => ..., [])
is equivalent touseRef((...) => ...).current
.)So are you saying that we can function to setValue? And it will call it with the current state? Just like the old this.setState()?
How could I miss it!
play with this code and see where it goes wrong
I think I am missing what you are trying to accomplish here? For me everything works as expected.
If you want to know why is
console.log('here')
being called? It's because callbacks are regenerated on each render, this way you can always access fresh values from your closure. But values inside the handlers object are always the same, so you are passing the same value to your components.To visualise:
[handler] calls [callback]
[callback] changes on each render
[handler] always stays the same
[handler] is what you are passing to your descendant components.
[callback] is the function that does stuff.
If I will click second button, I will see
here
in the console e.g. function in your hook gets recreated even though state connected to it isn't changed. But I see from the comment, that this is exactly what you want¯\_(ツ)_/¯
You don't need to put the handlers into your ref. I provided the following example in a reply to your previous post. You can solve this with a simple 4-line change, which will also be a lot easier for other developers to understand and work with.
I see.
My solution was is definitely not the best one here.
I'd like to encourage you to check @shiatsumat answer above. It's probably the cleanest way to approach this problem, this method was even mentioned in the official react docs!
The problem with your solution is that it's not clear what valueRef.current is and how to handle it. Some developers might try setting
valueRef.current
directly, and be surprised that it does not work.@shiatsumat solution allows you to write the code like this, but without valueRef. Just a single custom hook.