- Published on
Quick look into the useEffectEvent
The new hook in React 19.2
- Authors
- Name
- Nico Prananta
- Follow me on Bluesky
React just released the new version 19.2.0 which brings several new features including the new useEffectEvent hook. The doc says that it is a React Hook that lets you extract non-reactive logic from your Effects into a reusable function called an Effect Event.
Thanks to a quick chat with Dan Abramov, I got to realize that my understanding of the hook was initially incorrect.
At first I thought it's a way to create a "stable" function that always "sees" the latest state and props. But Dan explained that it actually creates an "unstable" function. It is basically a way to create a function that
- always "sees" the latest state and props
- doesn't cause an Effect that contains the function to re-run even though the function is "unstable"
Let's take a look at this example below which I adopted from the React doc. We have a notify
function (line 32-34) that is created by wrapping the showNotification
function with useEffectEvent
. The notify
function is always "unstable" because it always uses the latest theme
value. And even though it's used inside the useEffect
hook (line 38), it doesn't cause the useEffect
to re-run even though the notify
function always changes whenever the theme
value changes. And thanks to the new eslint rule, the notify
function doesn't need to be included in the dependency array of the useEffect
hook (line 44).
'use client'
import { useEffect, useState, useEffectEvent } from 'react'
const createConnection = (serverUrl: string, roomId: string) => {
return {
on: (eventName: string, callback: () => void) => {
console.log(`Event ${eventName} occurred: ${serverUrl} and roomId: ${roomId}`)
callback()
},
connect: () => {
console.log(`Connected to server: ${serverUrl} and roomId: ${roomId}`)
},
disconnect: () => {
console.log(`Disconnected from server: ${serverUrl} and roomId: ${roomId}`)
},
}
}
const serverUrl = 'wss://example.com'
const roomIds = ['123', '456', '789']
const themes = ['light', 'dark']
const showNotification = (message: string, theme: string) => {
console.log(`Notification: ${message} with theme: ${theme}`)
}
export default function Page() {
const [theme, setTheme] = useState<string>(themes[0] || '')
const [roomId, setRoomId] = useState<string>(roomIds[0] || '')
const notify = useEffectEvent((message: string) => {
showNotification(message, theme)
})
useEffect(() => {
const connection = createConnection(serverUrl, roomId)
connection.on('connected', () => notify('Connected!'))
connection.connect()
return () => {
connection.disconnect()
}
}, [roomId])
return (
<div className="flex min-h-svh flex-col items-center justify-center">
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
{themes.map((theme) => (
<option key={theme} value={theme}>
{theme}
</option>
))}
</select>
<select value={roomId} onChange={(e) => setRoomId(e.target.value)}>
{roomIds.map((roomId) => (
<option key={roomId} value={roomId}>
{roomId}
</option>
))}
</select>
{/* <ChatRoom serverUrl={serverUrl} roomId={roomId} theme={theme} /> */}
{`ChatRoom: ${serverUrl} and roomId: ${roomId} and theme: ${theme}`}
</div>
)
}
This example made me think that this could be achieved without the useEffectEvent
hook by simply creating the notify
function outside of the useEffect
hook and not including it in the dependency array of the useEffect
hook. Granted that the eslint will complain about the missing dependency, but I thought I could just ignore it.
export default function Page() {
const [theme, setTheme] = useState<string>(themes[0] || "");
const [roomId, setRoomId] = useState<string>(roomIds[0] || "");
const notify = (message: string = "Connected!") => {
showNotification(message, theme);
};
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on("connected", () => notify("Connected!"));
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
return (
// ...same as above
);
}
And I was partially right. Say initially the theme
is light
then I changed it to dark
. As expected, the connection didn't reconnect. When I selected different roomId
values, the connection reconnected and the notify
function was called, and the notification was shown with the dark
value. That's how it should be.
But Dan then pointed out that while that example works as it should be, the notify
function doesn't see the latest theme
value. To prove this, he told me to call the notify
function after the theme
value is changed. So I made the following changes which calls the notify
function periodically after the connection is established (line 15-17).
export default function Page() {
const [theme, setTheme] = useState<string>(themes[0] || "");
const [roomId, setRoomId] = useState<string>(roomIds[0] || "");
const notify = (message: string = "Connected!") => {
showNotification(message, theme);
};
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on("connected", () => notify("Connected!"));
connection.connect();
const timer = setInterval(() => {
notify("Timer");
}, 5000);
return () => {
connection.disconnect();
clearInterval(timer);
};
}, [roomId]);
return (
// ...same as above
);
}
As Dan explained, the notify
function inside the Effect doesn't change which means it never sees the latest theme
value. When I changed the theme
value to dark
, the notify
function inside the Effect still sees the light
value. This is a bug and it can be fixed by simply wrapping the notify
function with useEffectEvent
hook.
export default function Page() {
const [theme, setTheme] = useState<string>(themes[0] || "");
const [roomId, setRoomId] = useState<string>(roomIds[0] || "");
const notify = useEffectEvent((message: string = "Connected!") => {
showNotification(message, theme);
};
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on("connected", () => notify("Connected!"));
connection.connect();
const timer = setInterval(() => {
notify("Timer");
}, 5000);
return () => {
connection.disconnect();
clearInterval(timer);
};
}, [roomId]);
return (
// ...same as above
);
}
This is cool because now we can use a function that always "sees" the latest state and props without causing an Effect that contains the function to re-run. I'm looking forward to seeing how I and others will use this hook in the future.