Data & the Outside World

Lesson 1 of 8

Effects vs Events

"Effects are an escape hatch from the React paradigm." — React documentation, You Might Not Need an Effect

Somewhere in every growing React codebase there is a useEffect that watches a piece of state, sets another piece of state, and quietly ruins someone's afternoon. It renders twice, it flashes stale data, occasionally it loops forever. The developer who wrote it wasn't careless — they'd simply learned that useEffect is "where you put code that reacts to things," and reacting to things is most of programming. This chapter draws the one line that dissolves that whole category of bugs: the line between code that runs because the user did something and code that runs because the component was rendered with certain values. Get that line right and you'll write far fewer Effects — and the ones you keep will be the ones that were always supposed to be there.

The question this chapter answers

You need something to happen. Do you put it in an event handler, or in an Effect? Ask it wrong and you get double renders, stale data, and infinite loops. So how do you tell — every time — which one you actually need?

Core Concepts

There are exactly two places reactive logic runs in React, and they answer two different questions.

An event handler runs because of a specific interaction. The user clicked this button, submitted this form, typed this key. The code inside onClick runs once, in response to that one event, and never on its own. If you can point at the interaction that caused the code to run — "it happened because they clicked" — it belongs in a handler.

An Effect runs because the component rendered with certain values. It doesn't care who or what caused the render; it fires because the component is now on screen displaying some state, and something outside React must be brought into agreement with it. A chat room is open, so a connection must exist. A search term is set, so the document title should match. Effects are for synchronization, and synchronization is caused by the component being displayed — not by any single interaction.

So the entire decision reduces to one question you ask of every piece of logic:

Everyday analogy

A handler is a doorbell; an Effect is a thermostat. The doorbell rings because someone pressed it — one press, one ring, a direct cause you can name. The thermostat isn't triggered by anyone; it runs continuously because the room exists at some temperature and must be kept in sync with the target on the dial. Ask of any code: "did a person just do this (doorbell → handler), or is this just keeping the world in step with what's on screen (thermostat → Effect)?" You almost never confuse the two objects in real life. The confusion in React comes only from both living near useState.

Is it an interaction, or a render?

Put the two side by side. "When the user clicks Buy, send the order" — that is caused by a click. It goes in onClick, full stop. There is no Effect anywhere in a correct implementation, because nothing outside React needs ongoing synchronization; a discrete thing happened once. "This chat room component is showing room #7, so a connection to room #7 should be open" — that is caused by the component rendering with roomId = 7. It goes in an Effect, because the connection must track the room across every render, including ones the user didn't trigger.

The trap is the middle case that looks like an interaction but gets miswired as an Effect. Watch:

React — Try it Yourself
Loading editor…
Preview

Every click now costs two renders: one that adds the item, then the Effect fires after paint and sets count, forcing a second. And count is a lie for one frame — it holds the old value until the Effect catches up. The fix isn't a better dependency array. It's recognising that count is not an external system at all — it's items.length, a value you can compute on the way to the screen:

React — Try it Yourself
Loading editor…
Preview

One render, always correct, impossible to desync. This is the tell: if a value can be computed from props and state, it is not an external system, so no Effect can be justified.

You Might Not Need an Effect — the field guide

The buggy cart above is one instance of a family. Here is the whole family, with the fix for each — memorise this table, because it is the most interview-dense knowledge in this Volume:

You're tempted to… …with an Effect that Do this instead
Transform data for rendering stores a filtered/formatted value in state, refreshes it Compute it during render as a plain const (memoise only if truly expensive)
Reset state when a prop changes watches the prop, clears the state Pass a different key so React remounts and resets it for you
Adjust some state when a prop changes watches the prop, calls setState Rare: set state during render (guarded by a comparison), or restructure to derive it
Handle a user event watches the state the event changed, then acts Put the logic in the event handler where the interaction happened
Chain state updates sets state, which triggers another Effect that sets more state Compute the whole result and set it once, in the handler

The last row is the nastiest in production. A cascade of Effects — A sets B, an Effect on B sets C, an Effect on C fires a request — is impossible to read, re-runs more than you think, and is fragile to reorder. Whenever a single interaction should produce several updates, do all of it in that interaction's handler, top to bottom, where the flow is linear and visible.

The engineer's notebook

The four questions an experienced React dev runs before writing useEffect:

• "Can a user point at what they did to cause this?" — If yes, it's a handler. Effects have no such trigger.
• "Is this value derivable from props and state?" — If yes, compute it in render; storing it in state is the bug.
• "Am I resetting state because a prop changed?" — Reach for key before you reach for an Effect.
• "Is one Effect only firing because another Effect set state?" — Collapse the chain into the handler that started it.
• "If none of the above — what external system am I syncing with?" — Name it out loud. If you can't, delete the Effect.

So what are Effects genuinely for?

After you've removed the impostors, a real and important job remains — and it's always the same shape: reaching outside React to something React can't see or control. Legitimately:

  • Subscriptions — a WebSocket, an event listener, matchMedia, a store subscription. Setup opens it; cleanup closes it.
  • Network synchronization — keeping server state in step with UI state (the subject of the very next chapter).
  • Non-React widgets — a map, a chart, a video player you must imperatively .init() and .destroy().
  • Browser APIs — document.title, localStorage, focus management, observers.
  • Timers — setInterval / setTimeout that must start and stop with the component.

Notice every item is a system with a lifecycle of its own that must be opened and closed. That's the signature of a real Effect: it has cleanup because it has something to stop.

The awkward middle: reading latest values without re-syncing

Sometimes an Effect genuinely synchronizes and needs to read a value that shouldn't cause a re-sync. Classic case: a chat connection that should reconnect when roomId changes, but not when a theme prop changes — yet the "on connect" logic wants to show a notification in the current theme. Put theme in the dependency array and you reconnect on every theme switch; leave it out and you read a stale theme (and the linter rightly complains).

React's answer, currently experimental, is an Effect Event created with useEffectEvent: a function you call from inside the Effect that always "sees" the latest props and state but is not reactive — it never goes in the dependency array and never triggers a re-sync.

// Experimental API — not stable in React 19; shown for the mental model only.
const onConnected = useEffectEvent(() => {
  showNotification("Connected!", theme); // always the LATEST theme, non-reactive
});

useEffect(() => {
  const conn = createConnection(roomId);
  conn.on("connected", () => onConnected());
  conn.connect();
  return () => conn.disconnect();
}, [roomId]); // re-syncs on roomId ONLY — theme changes don't reconnect

The name is deliberate: an Effect Event is the "event-like" part inside an Effect — non-reactive logic that reads fresh values. Know the concept and the problem it solves; do not build production code on it yet, and never reach for it before you've checked whether the value belongs in the dependency array in the first place.

History changed here

The "You Might Not Need an Effect" guidance was one of the most consequential documentation changes React ever made. For years the community treated useEffect as the general-purpose "run code in response to change" hook, and codebases filled with Effect chains nobody could follow. When the React team at Meta rewrote the docs (2023) around synchronization, they made the radical claim explicit: most Effects people write are mistakes. The same rewrite introduced "Separating Events from Effects" — the exact distinction this chapter is built on — and previewed useEffectEvent to handle the one case the old model couldn't express cleanly. The API barely changed; the teaching changed everything.

Real-world example — chika.ng Academy

On a lesson page (a Next.js + React app over Drizzle/MySQL), "mark lesson complete" is a button click that POSTs to the API — pure event logic, so it lives in the onClick handler, not in an Effect watching a "completed" flag. But the "students online" badge opens a subscription that must track which lesson you're viewing — that's caused by the component rendering with a lesson id, so it's a real Effect keyed on that id, with cleanup that closes the connection. Early on, a well-meaning refactor moved the POST into an Effect that watched the completion state; the result was a double network call on StrictMode's dev double-render and a race on fast clicks. Moving it back into the handler — where the interaction actually was — fixed both. Same lesson taught twice: the click was never a render.

Common Mistakes

  • Using an Effect to derive data for rendering. Storing items.length (or a filtered list, or a formatted string) in state and syncing it in an Effect double-renders and flashes stale values. Compute it during render.
  • Handling an event in an Effect that watches state. "POST when formData changes" should be "POST in the submit handler." An Effect can't tell which interaction changed the state, so it also fires on renders you didn't mean.
  • Resetting state with an Effect instead of a key. Watching a prop to clear child state is fragile; a changing key remounts the child and resets it for free.
  • Building chains of Effects. Effect A sets state that triggers Effect B that triggers Effect C. Collapse the cascade into the single handler that started it, where the logic reads top-to-bottom.
  • Assuming "reactive" means "Effect." Rendering derived values is reactive — React re-runs your component and recomputes them. Reactivity is not evidence you need useEffect.
  • Reaching for useEffectEvent too early. It's experimental and solves a narrow, real problem (reading latest values without re-syncing). If the value should trigger a re-sync, it just belongs in the dependency array.
Thought experiment

A form has an Effect: "when isSubmitted becomes true, send the analytics event and show the toast." It works in testing. Then you add a "resubmit" feature and a bug appears: the toast sometimes fires on mount, or twice, or when unrelated state changes. Why? Because isSubmitted is state, and an Effect watching state fires whenever anything re-runs the Effect — including StrictMode's dev double-invoke and any render where React re-commits. The Effect can't see that a submit interaction is what you meant; it only sees a value. Move both the analytics call and the toast into the onSubmit handler — the one place that genuinely knows a submission happened — and every phantom fire disappears. The question was never "when did isSubmitted change?" It was "when did the user submit?"

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
"What's the difference between putting logic in an event handler versus a useEffect?"
Model answer — "An event handler runs because of a specific interaction — a click, a submit — so logic caused by the user goes there. An Effect runs because the component rendered with certain values, and its job is to synchronize with something outside React, like a subscription or the document title. I ask: did a user do this (handler), or is this keeping the outside world in sync with what's on screen (Effect)?"
Follow-up they'll throw: "Where does 'send the form when the user clicks submit' go?" → The submit handler. Nothing outside React needs ongoing sync, so there's no Effect at all.
Intermediate
"Name the common 'you might not need an Effect' anti-patterns and their fixes."
Model answer — "Four big ones. Transforming data for rendering — compute it during render, don't store it in state and sync it. Resetting state when a prop changes — use a key to remount. Handling a user event — put it in the handler, not an Effect watching the state it changed. And chains of Effects that set state to trigger more Effects — do the whole thing in the one handler that started it. The unifying rule: if there's no external system, there's no Effect."
Follow-up: "Why is the derived-state-in-an-Effect version actually worse, not just uglier?" → It renders twice per change and flashes a stale value between the two renders.
Senior
"You have an Effect that must reconnect a chat socket when roomId changes but must not reconnect when the theme prop changes — yet the connect handler needs the current theme. How do you model that?"
Model answer — "That's the exact case that separates reactive from non-reactive logic inside one Effect. The connection itself is reactive to roomId, so roomId is the only dependency. Reading the current theme when the socket connects is event-like — it should see the latest value but must not cause a re-sync. React's direction for this is an Effect Event via useEffectEvent: a non-reactive function called from inside the Effect that always reads fresh props and state and is deliberately excluded from the dependency array. It's still experimental, so in production today I'd use a ref to hold the latest theme, or accept the tradeoff — but I'd name it as the reactive-vs-event boundary, which is the real concept."
The senior signal: framing it as reactive-vs-non-reactive logic within a legitimate Effect, and being honest about the API's stability rather than cargo-culting it.
Company lens — how the same topic gets asked
micro1Live and conversational — reason out loud. Expect a code sample with a misplaced Effect and "what's wrong here?" Narrate the interaction-vs-render question as you diagnose, rather than jumping straight to the fix.
MetaReact's authors — expect first-principles depth. Be ready to explain why derived-state Effects double-render, how key resets state, and the reactive-vs-event distinction behind useEffectEvent. "It just re-runs on change" won't satisfy here.
GoogleWants precision and correctness — spotting the Effect that shouldn't exist in a code review, and stating exactly which fix (render-time compute, key, or handler) applies to which anti-pattern.
Amazon / AWSOperational lens: "an Effect double-fired a payment in production — how did it happen and how do you prevent it?" Tie it to event logic misplaced in an Effect, StrictMode surfacing it in dev, and moving the action into the handler.
Hands-on exercise
  1. In the BuggyCart editor above, add a console.log("render") at the top of the component and click "Add apple" a few times. Count the renders per click, then switch to the GoodCart version and count again. Say out loud (as in a micro1 screen) exactly why the buggy one renders twice.
  2. Take a component that stores fullName in state and updates it in an Effect whenever firstName or lastName props change. Delete the state and the Effect and derive fullName during render. Then articulate the two bugs you just removed.
  3. Write down three pieces of logic from any app you know and label each "handler" or "Effect," and for the Effects, name the external system in one sentence. If you can't name a system, reclassify it.

Chapter Summary

  • Event handlers run because of a specific interaction; Effects run because the component rendered with certain values. That single distinction decides where logic goes.
  • Ask of every piece of logic: did a user do this (→ handler), or is this keeping the outside world in sync with what's displayed (→ Effect)?
  • Most Effects shouldn't exist. Transform data during render; reset state with a key; handle events in handlers; collapse Effect chains into one handler.
  • A derived-state Effect isn't just ugly — it double-renders and flashes a stale value. If a value is a pure function of props and state, no Effect is justified.
  • Real Effects synchronize with external systems that have their own lifecycle: subscriptions, network sync, non-React widgets, browser APIs, timers — the ones that need cleanup.
  • useEffectEvent (experimental) reads the latest values inside an Effect without re-syncing — the reactive-vs-event boundary. Know it; don't yet depend on it.
Narrative bridge

We've drawn the line between interactions and renders, and cleared out the Effects that never belonged. What's left is the single most important thing Effects are genuinely used for — reaching across the network to a server React can't see. It's also where the subtleties multiply: race conditions, loading and error states, cleanup on the in-flight request, and the modern tools that increasingly want to take the job away from Effects entirely. That's next: Fetching Data.

Editorial reflection — the phrase "you might not need an Effect" sounds like a limitation, but it's a liberation. Every Effect you delete is a render you didn't waste, a stale flash the user never saw, a race that can't happen, a chain nobody has to trace at 2am. The skill this chapter teaches isn't writing Effects — it's recognising when you're not synchronizing with anything at all, which turns out to be most of the time. Master engineers aren't the ones with the cleverest useEffect; they're the ones whose components barely have any, because they kept asking the quiet question — "was this the user, or was this the render?" — until the answer became automatic.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.