React hooks & the closure hell
Since Facebook introduced functional components and hooks, event handlers become simple closures. Don't get me wrong, I like functional components, but there is a number of issues that niggle at me, and when I ask about them in the community, the most common answer is: "don't worry about premature optimizations".
But that is the problem for me, I grew up programming in C, and I frequently worry about the performance of my applications, even if others find it less significant.
The problem?
Since event handlers are closures we need to either re-create them on each render or whenever one of it's dependencies changes. This means components that only depend on the event handler (and possibly not on the handler's dependencies) will have to re-render too.
Consider this example code (Try here):
import React, { useState, useCallback, memo } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
let times = 0
const ExpensiveComponent = memo(({ onClick }) => (
<p onClick={onClick}>I am expensive form component: {times++}</p>
))
const App = () => {
const [value, setValue] = useState(1);
const handleClick = useCallback(
() => {
setValue(value + 1)
},
[value],
);
return (
<div className="app">
<ExpensiveComponent onClick={handleClick} />
<button onClick={handleClick}>
I will trigger expensive re-render
</button>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
In the previous example, clicking on the button will cause ExpensiveComponent
to re-render. In case of class based components it would be unnecessary.
Solution?
The experimental tinkerer I am I tried to find the solution to this problem, solution where we can use functional components, but don't have to create a new callback every time we create a new value.
So I created useBetterCallback(fn, deps)
. The signature for this function/hook is identical to useCallback(fn, deps)
. The difference is that it will always return the same identical handler no matter what.
Some of you might think: 'So how do I access fresh state values?'. useBetterCallback
will call your handler with one additional argument, and that argument is an array with all dependencies your callback depends on. So instead of recreating the callback we pass new values to existing one.
Here is the source code for the useBetterCallback
hook.
const useBetterCallback = (callback, values) => {
const self = useRef({
values: values,
handler: (...args) => {
return callback(...args, self.current.values)
}
});
self.current.values = values
return self.current.handler
}
And here is an example of the useBetterCallback
in action (Try here):
import React, { useState, useRef, memo } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const useBetterCallback = (callback, values) => {
const self = useRef({
values: values,
handler: (...args) => {
return callback(...args, self.current.values)
}
});
self.current.values = values
return self.current.handler
}
let times = 0
const ExpensiveComponent = memo(({ onClick }) => (
<p onClick={onClick}>I am expensive form component: {times++}</p>
))
const App = () => {
const [value, setValue] = useState(1);
const handleClick = useBetterCallback((event, [ value, setValue ]) => {
setValue( value + 1 )
}, [value, setValue])
console.log("Value: " + value)
return (
<div className="app">
<ExpensiveComponent onClick={handleClick} />
<button onClick={handleClick}>
I will not trigger expensive re-render
</button>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Review?
What do you think?
Top comments (8)
This is a lot more code than you need. Taking your original sample, put
value
into a ref fromuseRef
, and inside theuseCallback
function, getvalue
from your ref. Also, don't use[value]
as a dependency, use only[]
dependencies, this way it only runs once.There is another way to go about this, maybe cleaner for some people.
It can be used like this (Try here):
This version will be nicer for people who preferred old-school OOP programming. You've got the object, and you can set whatever you want there.
Example
How can I use effect in this?
Or just with normal useEffect with dependencies [self.state.value]?
Should work fine.
But I advise you try the new, cleaner version: dev.to/anpos231/react-hooks-the-cl...
React team actually discurrages this patern: reactjs.org/docs/hooks-faq.html#ho...
Instead of the pattern you use you can pass a callback to the state setter:
How about this one?
I love how it has a similar interface to the original useCallback(). I saw your update, but I still like this simple replacement. I was headed down a similar road using refs, but I like how the logic is encapsulated in the hook. I was going nuts trying to optimize my app, where event handlers are almost always dependent on props, causing them to be regenerated and causing re-renders of children.
This also simplifies the dependencies, since you don't need to provide full paths such as [state.x.y.z, ...]. You can just pass [state], and you get a nice snapshot of everything, quickly, since it's immutable. As others has noted, it heads toward simulating dynamic scoping, a.k.a "this" in object-orientation.
Thanks again for the inspiration!