Effects are the most misused Hook; knowing what they are for prevents bugs, wasted renders and race conditions.
An Effect lets a component synchronize with a system outside React: a network connection, a browser API, a third-party widget, a subscription. It runs after React has committed the render to the screen:
useEffect(() => {
const conn = createConnection(roomId);
conn.connect();
return () => conn.disconnect(); // cleanup
}, [roomId]); // dependenciesThink of it as "keep this external thing in sync with these values", not as lifecycle events. The dependency array lists every reactive value the effect reads (props, state, and anything computed from them). React re-runs the effect after a render where a dependency changed (compared with Object.is), calling the previous cleanup first. Without the array, it runs after every render; with [], only on mount and cleanup on unmount.
Don't lie about dependencies; the eslint-plugin-react-hooks rule checks them. If an effect re-runs too often, change the code (move an object inside the effect, use an updater function) rather than omitting a dependency. For logic that should read the latest values without re-triggering the effect, React 19.2 adds useEffectEvent.
Cleanup must undo setup, because Strict Mode deliberately runs setup, cleanup, setup on mount to verify it.
You might not need an Effect. Effects are for external systems. Don't use one to:
- compute derived data (
useEffect(() => setFull(first + last))); compute it during render; - respond to a user event (send the POST in the submit handler, not in an effect watching a
submittedflag); - reset state when a prop changes; give the component a
keyinstead.
Each unnecessary effect adds an extra render and a window where the UI shows stale values.
Example
import { useEffect, useState } from "react";
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const socket = new WebSocket(`wss://chat.example.com/rooms/${roomId}`);
socket.onmessage = e => setMessages(m => [...m, e.data]); // updater: no dep on messages
return () => socket.close(); // runs before the next setup and on unmount
}, [roomId]); // reconnect only when the room changes
return <ul>{messages.map((m, i) => <li key={i}>{m}</li>)}</ul>;
}
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return <p>{width}px</p>;
}
export default function App() {
const [room, setRoom] = useState("general");
return <><button onClick={() => setRoom("random")}>Switch</button><ChatRoom roomId={room} /><WindowWidth /></>;
}Remember
- Effects synchronize with external systems and run after the commit
- List every reactive value the effect reads in its dependencies; return a cleanup that undoes setup
- Derived data belongs in render, user-triggered work in event handlers, not in effects
Watch out for
- Omitting dependencies to stop an effect re-running, which leaves it reading stale values.
- Chains of effects that set state to trigger other effects instead of computing values during render.