Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~24 min· topic 6 of 9

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 key makes 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 onChange so 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.

Form.jsxwhole filejsx
// 🔴 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).

TodoList.jsxwhole filejsx
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.

ProfilePage.jsxwhole filejsx
// 🔴 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.

Product.jsxwhole filejsx
// 🔴 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.

Game.jsxwhole filejsx
// 🔴 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 state

06Initialise 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.

main.jsxwhole filejsx
if (typeof window !== "undefined") {
  // runs once, when this module is first imported in the browser
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}
Initialise the app once, outside componentsdiagram
Rendering diagram…

Try it yourself

  1. 1

    Change the filter

    In the first example, render <VisibleTodos todos={todos} filter="all" />. Predict the count shown and whether filtering logs on the re-render. (3 todos; no extra log.)

  2. 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. 3

    Lift the state up instead

    Make the toggle fully controlled: the parent passes on and onChange, and the toggle has no state at all. Now there is nothing to sync (Topic 6.3).

Code & diagrams

Derived data: an effect costs an extra render New tab

`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

Press Run to see the page.

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"
Reset state with a key instead of an effect New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

user 1 comment: "Great work!"
user 2 comment: ""
Notify the parent in the handler, not in an effect New tab

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

Press Run to see the page.

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: true

Break 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.

terminal
$ Preview
── what you'll see ──
The comment is cleared, but only after one render that showed user 2's page with user 1's comment. With slow rendering you can see the old text flash.

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 a key or calculation can't.

  • ▸

    The React Compiler (Phase 16) memoises calculations automatically, which removes most manual useMemo and makes "just calculate it during render" even cheaper.

  • ▸

    eslint-plugin-react-hooks 7 includes a set-state-in-effect rule (compiler-powered) that flags synchronous setState calls 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. 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. 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 when a or b changes 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. 3

    Resetting state when a prop changes: don't write an effect that clears state when userId changes. 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. 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. 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 own setState; React then batches both updates into one render instead of two.

  6. 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

01

Why is storing derived data in state and updating it with an effect a mistake?

02

How do you reset all of a component's state when a prop changes, without an effect?

03

A child calls onChange from an effect when its state changes. What's wrong, and what's the fix?

04

Where should code go that must run exactly once when the app loads?

Practice

01

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>; }

02

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, useMemo trades a little memory for skipped calculations. Measure before adding it.

  • ↔

    Resetting with key throws 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 useMemo only 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.