Synchronizing with Effects
Use useEffect to keep a component in sync with something outside React, and clean up after yourself.
Most of your code so far lives in two places. Rendering code turns props and state into JSX, and must be pure. Event handlers run when the user does something, like clicking a button.
Some code fits neither. Say a chat room component must connect to a chat server whenever it is on screen. Nobody clicked anything; the connection should exist because the component is displayed. That is what Effects are for: code that runs after rendering, to synchronize your component with a system outside React.
Your first Effect
An "external system" is anything React doesn't control: a network connection, a browser API like setInterval or addEventListener, a third-party map widget, or localStorage. Here is an Effect that keeps a clock running with setInterval:
There are three parts, and you will write them in this order:
- Declare the Effect. Call
useEffectwith a function. React runs that function after the component appears on screen. - Specify the dependencies. The array at the end tells React when to run the Effect again. An empty array
[]means "only when the component first appears". - Add cleanup if needed. The function you return is the cleanup. React calls it before running the Effect again and when the component disappears. Here it stops the interval.
Dependencies: when should the Effect re-run?
The dependency array decides how often your Effect runs:
useEffect(() => {
// Runs after EVERY render
});
useEffect(() => {
// Runs only when the component first appears (mounts)
}, []);
useEffect(() => {
// Runs on mount, and again whenever roomId has changed since the last render
}, [roomId]);The rule is simple: every reactive value your Effect reads must be in the array. Reactive values are props, state, and anything calculated from them inside the component. You don't choose the dependencies; your code does. The eslint-plugin-react-hooks linter that comes with Vite's template checks this for you.
Here is the classic example. A chat room connects to whichever room you select. Open the console panel and switch rooms.
Watch the log when you change rooms: React disconnects from the old room (cleanup with the old roomId), then connects to the new one. Click "Close chat" and you see one final disconnect.
Why does it run twice?
When the sandbox first loaded, you probably saw this in the console:
Connecting to "general"...
Disconnected from "general"
Connecting to "general"...That is not a bug. In development, Strict Mode mounts every component, immediately unmounts it, and mounts it again. It is a stress test: if your cleanup correctly undoes your setup, the user can't tell the difference between "setup once" and "setup, cleanup, setup". If something breaks, you have found a missing cleanup before your users did. This only happens in development; production runs the Effect once.
With useEffect(fn, [roomId]), when does React run the cleanup function?
Fix the leaking keyboard listener
This component should log every key you press, once. Click the preview, press a few keys and check the console. Each key is logged twice, and toggling the logger off doesn't stop it. Add the missing cleanup.
Show solution
Return a cleanup function that removes the same handler function:
useEffect(() => {
function handleKeyDown(e) {
console.log('You pressed: ' + e.key);
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);Strict Mode's mount, unmount, mount now adds a listener, removes it, and adds it again, so exactly one listener is active. Turning the logger off unmounts KeyLogger, which runs the cleanup.
Deep diveuseEffectEvent: reading the latest value without re-running
Sometimes an Effect needs to read a value without reacting to it. Imagine showing a notification in the current theme when the chat connects. If you read theme inside the Effect, it becomes a dependency, so changing the theme would reconnect to the chat. That is wrong: the connection only depends on roomId.
React 19.2 added useEffectEvent for this. It wraps a piece of logic so it always sees the latest props and state, but doesn't make the Effect re-run:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // always the latest theme
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // theme is not a dependency anymore
}Two rules come with it. Don't put the Effect Event in the dependency array, and only call it from inside Effects; never pass it to other components or hooks. You won't need this often as a beginner, but it is the right tool when the linter pushes a dependency that you know shouldn't restart the Effect.
Day 4 progress