This article needs prior understanding of Hooks. So, if you are not already familiar please checkout the official docs.
I came across this hook while reviewing a PR of one of my colleague ciprian. One of my colleague introduced a hook in his PR and we had a long healthy technical discussion around that as it was solving this issue.
Motivation for introducing useEvent
The React core team published a Request for Comment (RFC) for a new React hook: useEvent
. This post attempts to capture what this hook is and how it works.
Also here is the proposal for useEvent hook and its not been released yet (by the time I am writing this post) and its behavior could change.
The real problem
let’s wrap our heads around the problem before we jump into what useEvent
is. React’s execution model is largely powered by comparing the current and previous values of state
/props
. This happens in components and in hooks like useEffect
, useMemo
, and useCallback
.
Lets see this example to make better understanding of the problem.
Example
This onClick
event handler needs to read the currently typed text:
function Chat() {
const [text, setText] = useState('');
// 🟡 Always a different function
const onClick = () => {
sendMessage(text);
};
return <SendButton onClick={onClick} />;
}
Let's say you want to optimize SendButton
by wrapping it in React.memo
. For this to work, the props need to be shallowly equal between re-renders. The onClick
function will have a different function identity on every re-render, so it will break memoization.
The usual way to approach a problem like this is to wrap the function into useCallback
hook to preserve the function identity. However, it wouldn't help in this case because onClick needs to read the latest text:
function Chat() {
const [text, setText] = useState('');
// 🟡 A different function whenever `text` changes
const onClick = useCallback(() => {
sendMessage(text);
}, [text]);
return <SendButton onClick={onClick} />;
}
The text changes with every keystroke, so onClick
will still be a different function on every keystroke. (We can't remove text from the useCallback
dependencies because otherwise the onClick
handler would always "see" the initial text.
What's the solution
A new hook useEvent
is being proposed to ensure we have a stable reference to a function without having to create a new function based on its dependents.
What is useEvent
hook?
A Hook to define an event handler with an always-stable function identity.
By comparison, useEvent
does not take a dependency array and always returns the same stable function, even if the text changes. Nevertheless, text inside useEvent
will reflect its latest value:
function Chat() {
const [text, setText] = useState('');
// ✅ Always the same function (even if `text` changes)
const onClick = useEvent(() => {
sendMessage(text);
});
return <SendButton onClick={onClick} />;
}
As a result, memoizing SendButton
will now work because its onClick
prop will always receive the same function.
When to use useCallBack?
You must be curious about what would be the use-cases where useCallBack
will be useful after we have the useEvent
hook? I had the same question back then.
Well, useCallaback
will be used for memoizing render functions. For example:
javascript
const renderItem = useCallback(() => (
<span>{itemName}</span>
), [itemName])
Why? because useEvent cannot be used for this scenario as it would keep the same reference for render function, and this is not desired, because we need a changing ref so that we know when to rerender.
Event handlers wrapped in useEvent
will throw if called during render. (Calling it from an effect or at any other time is fine.) So it is enforced that during rendering these functions are treated as opaque and never called. This makes it safe to preserve their identity despite the changing props
/state
inside.
You can see many more evidences on that issue and RFC itself, I am not listing those here to keep this post concise. Overall, I think this is going to be a great addition to the React ecosystem.
Here is what my colleague did to mock the same behavior that useEvent
would probably be doing soon.
export default function useEventCallback<Args extends unknown[], Return>(
fn: (...args: Args) => Return,
): (...args: Args) => Return {
// ref is not initialized, in order to ensure that we can't call this in the render phase
// ref.current will be undefined if we call the `fn` during render phase (as a render function)
const ref = useRef<(typeof fn) | undefined>(undefined);
useLayoutEffect(() => {
ref.current = fn;
});
return useCallback(
(...args: Args) =>
// make sure that the value of `this` provided for the call to fn is not `ref`
ref.current.apply(void 0, args),
[],
);
}