How it works
The explanation, with examples you can run.
The shape of an effect
import { useEffect, useState } from "react" type Connection = { connect(): void; disconnect(): void; on(event: string, fn: (msg: string) => void): void }declare function createConnection(roomId: string): Connection export function ChatRoom({ roomId }: { roomId: string }) { const [messages, setMessages] = useState<string[]>([]) useEffect(() => { const conn = createConnection(roomId) conn.on("message", (msg) => setMessages((prev) => [...prev, msg])) conn.connect() return () => conn.disconnect() // cleanup: undo exactly what setup did }, [roomId]) return <ul>{messages.map((m, i) => <li key={i}>{m}</li>)}</ul>}React runs the setup after the room first appears on screen. When roomId changes, it runs the cleanup with the old values (disconnect from the old room), then the setup with the new ones. When the component unmounts, it runs the cleanup one last time. In development, Strict Mode runs one extra setup-and-cleanup cycle right after mounting: if that breaks something, the cleanup is incomplete. Effects only run in the browser, never during server rendering.
Dependencies
The dependency array is a description of the code, not a setting: every prop, state variable and value computed from them that the effect reads must be listed, and the lint rule checks it. After each render, React compares each dependency with its previous value using Object.is and re-runs the effect if any changed. With no array, the effect runs after every render; with [], only on mount and unmount.
If an effect runs too often, change the code, not the list:
- An object or function created during render is new every time. Create it inside the effect, depend on its primitive fields, or memoise it.
- If the effect only needs state to compute the next state, use an updater (
setX((x) => ...)) and drop the dependency. - For a value the effect needs to read but shouldn't react to, such as the current theme used when logging, React 19.2 added
useEffectEvent.
What effects are not for
Many effects shouldn't exist:
- Derived data. A filtered list or a total is a calculation; do it during render (memoised if it is slow).
- Events. "When the user clicks Buy, send the order" belongs in the click handler. An effect that watches a
submittedflag fires again on remount, and it loses the information about what caused it. - Resetting state when a prop changes. Give the component a
keyinstead. - Chains of effects that set state which triggers other effects: each link is an extra render.
Fetching in an effect
If you fetch in an effect, handle responses that arrive out of order:
import { useEffect, useState } from "react" type User = { id: string; name: string } export function Profile({ userId }: { userId: string }) { const [user, setUser] = useState<User | null>(null) useEffect(() => { let ignore = false fetch(`/api/users/${userId}`) .then((res) => res.json()) .then((data: User) => { if (!ignore) setUser(data) }) return () => { ignore = true // a newer userId has taken over; drop this response } }, [userId]) return <p>{user ? user.name : "Loading..."}</p>}Without the flag, a slow response for an old userId can overwrite a newer one. Frameworks and data libraries handle caching, deduplication and this race for you, which is why they are usually a better choice for data fetching than hand-written effects.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.