If you've been writing React for a while, you've probably built a habit of
reaching for useEffect whenever something needs to "happen."
Data needs transforming? useEffect. A value depends on props?
useEffect. User clicks a button and something should update?
Also useEffect.
Most of these cases don't need an effect. Effects are for syncing with external systems like the DOM, a network request, or a browser API. If you're using them to derive state or respond to user events, you're adding complexity and extra re-renders for no reason.
This is the most common misuse. You have state that depends on other state or props, so you sync them with an effect:
// Don't do this
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);
useEffect(() => {
setCount(items.length);
}, [items]);
This triggers an extra render every time items changes. React
renders with the stale count, then the effect runs, updates
count, and React renders again. Two renders for something you
can compute directly.
Just calculate it:
// Do this instead
const [items, setItems] = useState([]);
const count = items.length;
No state. No effect. No extra render.
Same mistake, different shape. Transforming a list based on some filter:
// Don't do this
const [query, setQuery] = useState("");
const [filtered, setFiltered] = useState(todos);
useEffect(() => {
setFiltered(todos.filter((t) => t.text.includes(query)));
}, [todos, query]);
Two renders per change. Just compute it inline:
// Do this instead
const [query, setQuery] = useState("");
const filtered = todos.filter((t) => t.text.includes(query));
If the filtering is expensive, wrap it in useMemo:
const filtered = useMemo(
() => todos.filter((t) => t.text.includes(query)),
[todos, query]
);
useMemo caches the result. It doesn't schedule a state update
like useEffect + setState does.
When a user does something and you want to react to it, put that logic in the event handler, not an effect:
// Don't do this
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
sendAnalytics("form_submit");
setSubmitted(false);
}
}, [submitted]);
function handleSubmit() {
setSubmitted(true);
}
This is a roundabout way of saying "when the user submits, send analytics." Just put it in the handler:
// Do this instead
function handleSubmit() {
sendAnalytics("form_submit");
}
If something happens because of a user action, it goes in the event handler. If something happens because the component appeared on screen and you need to sync with something external, that's an effect.
A common pattern when a component receives a new ID and needs to reset its internal state:
// Don't do this
function ProfilePage({ userId }) {
const [comment, setComment] = useState("");
useEffect(() => {
setComment("");
}, [userId]);
// ...
}
This renders with stale state first, then the effect clears it. Use a
key instead to tell React this is a different component
instance:
// Do this instead
// In the parent:
<ProfilePage userId={id} key={id} />
When the key changes, React unmounts the old instance and
mounts a fresh one. State starts clean.
Effects are the right tool when you're syncing with something outside React:
// This is a valid use of useEffect
useEffect(() => {
const handler = (e) => setWindowWidth(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
Before writing useEffect, ask: "Am I syncing with something
outside of React?" If no, you probably don't need it.
useMemo.key.Effects are an escape hatch, not a default.