The previous week, Dan Abramov merged a new rfc about useEvent
. I propose you to look at this coming soon hook, I hope :)
Before reading this article, I recommend you to read my Things you need to know about React ref and When to use useCallback? if it's not already done.
Explanations of the problem
A first example
Have you ever felt that you add a dependency to a hook (useEffect
or useCallback
for example) not to have a stale closure but feeling that it's not good?
useEffect(() => {
const twitchClient = new TwitchClient();
twitchClient.connect();
twitchClient.on("message", (message) => {
if (shouldNotReadMessage) {
console.log(`The message is: ${message}`);
}
});
return () => twitchClient.disconnect();
}, [shouldNotReadMessage]);
Note: You can see me on Twitch where I do development and creative hobby.
Why I'm feeling bad about this code?
My client will disconnect / reconnect each time the shouldNotReadMessage
changes, which is odd because just using it in an event listener.
So I decide to use a React ref:
const [shouldNotReadMessage, setShouldNotReadMessage] =
useState(true);
const shouldNotReadMessageRef = useRef(
shouldNotReadMessage
);
// Do not forget to update the reference
// This `useEffect` has to be before the next one
useEffect(() => {
shouldNotReadMessageRef.current = shouldNotReadMessage;
});
useEffect(() => {
const twitchClient = new TwitchClient();
twitchClient.connect();
twitchClient.on("message", (message) => {
if (shouldNotReadMessageRef.current) {
console.log(`The message is: ${message}`);
}
});
return () => twitchClient.disconnect();
}, []);
No more disconnect / reconnect every time shouldNotReadMessage
changes but some boilerplate code.
It's possible to make a custom hook useStateRef
to mutualize the code, because it will be used often:
function useStateRef(state) {
const ref = useRef(state);
useLayoutEffect(() => {
ref.current = state;
});
return ref;
}
Note: You will have to put the
useStateRef
returned value as dependency if you want to satisfy the linter.
Previous example analysis
In the previous example, the callback that needs the latest value of the state shouldNotReadMessage
is an event listener. Because we want to execute the callback only when a message is received.
Most of the time, we work with event listener, their particularity is that their name can start by on
. You are probably more used to deal with DOM event listener, for example when adding an onClick
listener on a button.
Note: If you want to know how React handles DOM event listener you can read my article Under the hood of event listeners in React.
A second example
Have you ever deal with memoized components?
A memoized component optimizes re-render. The principle is simple: if there is no prop that has changed then the component does not render. It can be useful when dealing with component having costly renders.
Note: You can fine grained when the component should render. For example with
React.memo
it will be thanks to the second parameter. The default behavior is to render when any prop has changed.
So any references should be fixed.
So if you have the following code, the memoization is useless. Because each time the App
renders a new onClick
callback is created.
function App() {
const onClick = () => {
console.log("You've just clicked me");
};
return <MemoizedComponent onClick={onClick} />;
}
You have to use the useCallback
hook.
import { useCallback } from "react";
function App() {
const onClick = useCallback(() => {
console.log("You've just clicked me");
}, []);
return <MemoizedComponent onClick={onClick} />;
}
Note: In this example I could just extract the callback outside of the component, but we are going to complexify the code soon ;)
What happened if your callback needs an external variable?
Well it depends. If you want to access a ref it's totally fine. But if it's a state you will have to add it in the array dependency of useCallback
.
When this callback is an event listener then the problem is the same than before with useEffect
. It seems useless to recreate a new callback each time because will make the memoized component re-render because of that.
So we will use the useStateRef
hook implemented before.
Because of that you can have complex code. Trust me it happened to me :(
A last example
In my article When to use useCallback?, I tell that I try to always useCallback
functions that I return from hooks that will be used in multiple places, because I don't know the place where it will be used: in useEffect
? in useCallback
? in event listener?
But sometimes it's complicated to make a fully fixed reference.
So it can happen, like in the previous example, that an event listener that is memoized is recreated unnecessarily.
import { useCallback, useState } from "react";
function useCalendar() {
const [numberDayInMonth, setNumberDayInMonth] =
useState(31);
const [currentYear, setCurrentYear] = useState(2022);
const [currentMonth, setCurrentMonth] =
useState("January");
const onNextYear = useCallback(() => {
setCurrentYear((prevYear) => {
const nextYear = prevYear + 1;
if (currentMonth === "February") {
const isLeapYear = ... // some process with nextYear
const isLeapYear = false;
if (isLeapYear) {
setNumberDayInMonth(29);
} else {
setNumberDayInMonth(28);
}
}
return nextYear;
});
}, [currentMonth]);
// In a real implementation there will be much more stuffs
return {
numberDayInMonth,
currentYear,
currentMonth,
onNextYear,
};
}
In this case, a new callback for onNextYear
will be created each time currentMonth
changes.
Here again the solution would be to use the useStateRef
hook implemented before.
useEvent to the rescue
The solution to all the above problems is that React exposes a new hook probably named useEvent
that returns a memoized callback (with useCallback
) that called the latest version of our callback.
It's quite similar to the implementation I show earlier with useStateRef
but with callback.
An example of implementation would be:
function useEvent(handler) {
const handlerRef = useRef(null);
useLayoutEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args) => {
return handlerRef.current(...args);
}, []);
}
In reality, this will not use a useLayoutEffect
because it must run before other useLayoutEffect
so that they have the latest value of our callback for every case.
They will probably do an internal implementation to execute the update of the ref before all useLayoutEffect
.
As a reminder, useLayoutEffect
and useEffect
are executed from bottom to top in the tree. Started from the bottom 🎶
So, with the implementation above, we could have a stale callback in the following code and not log the right count
:
function Parent() {
const [count, setCount] = useState(0);
const onPathnameChange = useEvent((pathname) => {
// Note that we use a state value
console.log(
"The new pathname is:",
pathname,
"and count:",
count
);
});
return (
<>
<Child onPathnameChange={onPathnameChange} />
<button
type="button"
onClick={() => setCount(count + 1)}
>
Increment
</button>
</>
);
}
function Child({ onPathnameChange }) {
const { pathname } = useLocation();
useLayoutEffect(() => {
// Here we would have a stale `onPathnameChange`
// Because this is executed before the `useEvent` one
// So it can happen we have the previous `count` in the log
onPathnameChange(pathname);
}, [pathname, onPathnameChange]);
return <p>Child component</p>;
}
When not to use useEvent
?
Because the hook uses under the hood React reference it should not be called in render, due to problem we could encounter with Concurrent features.
For example a renderItem
callback should not be stabilized with useEvent
but with useCallback
.
Note: In this example it's logic because it's not an event handler.
Note:
useEvent
should be used when the callback can be prefixed byon
orhandle
.
Question I ask myself
The major question I have is: should it be the component / hook that declares the function that wraps in useEvent
or the component / hook that executes the callback?
I am sure that when using a memoized component it should be done at the declaration level, otherwise the memoization won't work:
function MyComponent() {
const onClick = useEvent(() => {});
return <MemoizedComponent onClick={onClick} />;
}
In other case, should we do at the declaration like today for useCallback
and make a nice documentation telling that it's an event callback?
I think the easiest solution will be at the execution side. Like this we can ensure that the behavior inside the component is the right we want without taking care of how a person uses this one.
The linter part of the RFC, goes in my way:
In the future, it might make sense for the linter to warn if you have
handle*
oron*
functions in the effect dependencies. The solution would be to wrap them intouseEvent
in the same component.
So it's likely that React pushes to use useEvent
at the call site.
Note: The drawback is that it can lead to re-declare new callback each times:
function Button({ onClick: onClickProp, label }) {
const onClick = useEvent(onClickProp);
return (
<button type="button" onClick={onClick}>
{label}
</button>
);
}
In any case, If it's done in both side, double wrap a callback with useEvent
should work too :)
Conclusion
I am really waiting for this new hook that will for sure simplify some code. I have already a lot of place in my codebase where it will help a lot.
Do not overused useEffect
when you can call some code in event listener just do it ;) Do not change a state, to "watch" it with a useEffect
.
Every callback that can be named with the prefix on
or handle
could be wrapped with this new hook but should we always do it?
Dan Abramov told in a comment that it could be the case, but it's not the aim of the RFC.
In the longer term, it probably makes sense for all event handlers to be declared with useEvent. But there is a wrinkle here with the adoption story with regards to static typing. We don't have a concrete plan yet (out of scope of this RFC) but we'd like to have a way for a component to specify that some prop must not be an event function (because it's called during render).
Maybe the name could change for something like useHandler
, because this is not returning an event but an handler.
Once the RFC is validated, the React team should work on recommendation about how to use it.
Are you hyped by this RFC? Do you have any questions?
To be continued :)
Do not hesitate to comment and if you want to see more, you can follow me on Twitch or go to my Website.
Top comments (0)