Topic 7.6
You Might Not Need an Effect
In one line
If no outside system is involved, you usually don't need an effect: calculate derived data during render (with useMemo if it's slow), reset state with a key, put user-caused logic in event handlers, and run app-wide setup once at module level.
Think of it like this
A shop that writes the total price on a sticky note, then a helper runs over after every change to fix the note. Why not just add up the basket when someone asks? The sticky note is extra state, the helper is an effect, and both are unnecessary. Most effects in real codebases are helpers fixing sticky notes.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Derived data
- A value that can be calculated from props or state, so it shouldn't be stored separately.
- useMemo
- A hook that caches the result of a calculation and recomputes it only when its inputs change.
- key (for resetting)
- Changing a component's
keymakes React treat it as new and reset all its state. - Chain of effects
- Effects that set state only to trigger other effects. Each link costs a render.
- Batching
- React combining several state updates from one event into a single render.
- Module level
- Code outside any function in a file. It runs once when the file is first imported.
- Notify the parent
- A child calling a function prop such as
onChangeso the parent can update its state.
Step by step
01Spot the sticky-note effect
An effect whose only job is to setState from other props or state is a sign of derived data.
// 🔴 extra state + extra render
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(first + " " + last);
}, [first, last]);
// ✅ calculate during render
const fullName = first + " " + last;02Cache it if it's slow
useMemo keeps the previous result while its inputs are the same. Only reach for it when a calculation is measurably slow (more than about 1 ms).
const visible = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter],
);03Reset with a key, not an effect
Instead of clearing state in an effect when userId changes, give each user their own component instance.
// 🔴 renders once with the old comment, then clears it
useEffect(() => {
setComment("");
}, [userId]);
// ✅ a new key means fresh state
<Profile key={userId} userId={userId} />04Move event logic into the handler
The notification belongs to the click. An effect would also fire when the page loads with the product already in the cart.
// 🔴 runs whenever product.isInCart is true, even on reload
useEffect(() => {
if (product.isInCart) showNotification("Added " + product.name);
}, [product]);
// ✅ runs because the user clicked
function handleBuyClick() {
addToCart(product);
showNotification("Added " + product.name);
}05Replace a chain with one handler
Calculate everything the next state needs in the handler that caused it. One event, one render.
// 🔴 card → goldCount → round → isGameOver: one effect per step
// ✅ all in the event
function handlePlaceCard(nextCard) {
setCard(nextCard);
if (nextCard.gold) {
if (goldCount < 3) {
setGoldCount(goldCount + 1);
} else {
setGoldCount(0);
setRound(round + 1);
}
}
}
const isGameOver = round > 5; // derived, not state06Initialise the app once, outside components
Code that must run once per page load goes at module level, where StrictMode and remounts can't repeat it.
if (typeof window !== "undefined") {
// runs once, when this module is first imported in the browser
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}Try it yourself
- 1
Change the filter
In the first example, render
<VisibleTodos todos={todos} filter="all" />. Predict the count shown and whetherfilteringlogs on the re-render. (3 todos; no extra log.) - 2
Remove the key
In the profile example, delete
key={userId}. Predict the second log line. (user 2 comment: "Great work!": the same component instance kept its state.) - 3
Lift the state up instead
Make the toggle fully controlled: the parent passes
onandonChange, and the toggle has no state at all. Now there is nothing to sync (Topic 6.3).
Code & diagrams
`BadName` first renders with an empty name (a flash of wrong UI) and needs a second render. `GoodName` is right the first time. `useMemo` skips the filtering on the unrelated re-render because `todos` and `filter` didn't change.
Preview
Console
Nothing logged yet.Expected console output
BadName render: ""
GoodName render: "Asha Rao"
filtering 3 todos
BadName render: "Asha Rao"
--- unrelated re-render
BadName render: "Asha Rao"
GoodName render: "Asha Rao"The script types a comment for user 1, then switches user. Because the key changed, React mounted a fresh `Profile` and the draft didn't leak to user 2. Remove `key={userId}` and the comment would stay.
Preview
Console
Nothing logged yet.Expected console output
user 1 comment: "Great work!"
user 2 comment: ""The effect version needs two render passes: the toggle renders, its effect tells the parent, then everything renders again. The event version updates both states in one handler, and React batches them into a single pass.
Preview
Console
Nothing logged yet.Expected console output
App render | a: false b: false
effect toggle render, on: false
event toggle render, on: false
--- click the effect toggle
effect toggle render, on: true
App render | a: true b: false
effect toggle render, on: true
event toggle render, on: false
--- click the event toggle
App render | a: true b: true
effect toggle render, on: true
event toggle render, on: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Reset state in an effect
In the profile example, remove the key and add useEffect(() => { setComment(""); }, [userId]); inside Profile.
Myth vs fact
Myth
Effects are the place to react to prop or state changes.
Fact
To react to a change *for the screen*, calculate during render. Effects are for syncing with systems outside React.
Myth
useMemo is an effect that stores a value.
Fact
useMemo runs during render and returns a cached value; it causes no extra render. An effect runs after render and setting state from it causes another.
Myth
Fetching data must be in an effect, so effects are always needed for data.
Fact
Fetching in an effect works (Topic 7.7), but frameworks and data libraries fetch in better places (Phase 14). And responding to a submit belongs in the submit handler.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Setting state during render (
if (prevItems !== items) { setPrevItems(items); setSelection(null); }) is allowed for the component being rendered: React throws away that render's output and re-renders immediately, before children render or the DOM updates. It's cheaper than an effect but harder to read; use it only when akeyor calculation can't. - ▸
The React Compiler (Phase 16) memoises calculations automatically, which removes most manual
useMemoand makes "just calculate it during render" even cheaper. - ▸
eslint-plugin-react-hooks 7 includes a
set-state-in-effectrule (compiler-powered) that flags synchronoussetStatecalls at the top of an effect body, the exact pattern this topic removes. - ▸
Data you subscribe to from outside React (a store, browser API) is best read with
useSyncExternalStore(Phase 15) instead of copying it into state from an effect, which avoids tearing during concurrent rendering.
Remember this
- 1
Derived data: if a value can be calculated from props or state, calculate it during render:
const fullName = first + " " + last. Storing it in state and syncing it with an effect is slower (React renders once with the stale value, runs the effect, then renders again) and buggier (for one render the screen shows the old value). This is the same rule as Topic 6.2: don't store what you can calculate. - 2
Expensive calculations: if the calculation is slow (filtering thousands of items), cache it with **
useMemo(() => calculate(a, b), [a, b])**. React re-runs the function only whenaorbchanges and otherwise returns the cached result. Still no effect and no extra state. Measure first: most calculations are fast enough to repeat (Phase 16). - 3
Resetting state when a prop changes: don't write an effect that clears state when
userIdchanges. Give the component a key:<Profile key={userId} userId={userId} />. A new key means React treats it as a different component, throws away the old state and starts fresh (Topic 6.5). To adjust only *part* of the state, calculate it during render (store an id instead of an object), or as a last resort set state during render guarded by a comparison with the previous value. - 4
Logic caused by an event belongs in the handler. Showing a notification after "Buy", sending a POST when a form is submitted: put them in the click or submit handler. In an effect they run at the wrong times (for example, again when the page reloads with the item already in the cart), and you lose the information about *which* event happened.
- 5
Chains of effects (an effect sets state A, which triggers an effect that sets B, which triggers another…) cause one render per link and are hard to follow. Calculate what you can during render and compute all the next state inside the event handler that started it. Notifying a parent (
onChange) should also happen in the handler, right next to the child's ownsetState; React then batches both updates into one render instead of two. - 6
Initialising the app once (loading settings, checking a login token) is not a component's job: StrictMode and remounts would run a component effect again. Run it at the top level of a module (it runs once when the module is first imported), or guard it with a module-level variable:
let didInit = false; ... if (!didInit) { didInit = true; init(); }. Remember the question from Topic 7.1: is there an outside system to sync with? If not, no effect.
Explain it without notes
Why is storing derived data in state and updating it with an effect a mistake?
How do you reset all of a component's state when a prop changes, without an effect?
A child calls onChange from an effect when its state changes. What's wrong, and what's the fix?
Where should code go that must run exactly once when the app loads?
Practice
Remove the effect:
``jsx
function Cart({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((s, i) => s + i.price, 0));
}, [items]);
return <p>Total: {total}</p>;
}
A <ChatThread threadId={id} /> keeps a draft and a scrollTop in state, and both must reset when the thread changes. Show the fix in the parent.
Trade-offs
- ↔
Calculating during render repeats the work every render; for genuinely slow work,
useMemotrades a little memory for skipped calculations. Measure before adding it. - ↔
Resetting with
keythrows away the whole subtree, including DOM nodes and child state; when you need to keep part of it, derive the rest during render instead.
Done when you can
I calculate derived values during render instead of syncing them with effects.
I use
useMemoonly for measurably slow calculations.I reset a component's state with a
key.I put user-caused logic and parent notifications in event handlers.
I can replace a chain of effects with one handler.
I know where app-wide, once-only setup goes.