Module 6: Effects And Side Effects
Module introduction: synchronizing with the outside world
Why this module exists here
In module 5 you learned the rule that organizes everything you've seen so far: derive, don't store. The product list filtered by the search doesn't live in its own state; it's computed in the render from products and query. And behind that rule there's a deeper idea: the render is a pure function. Data goes in (props and state), the UI description comes out, and along the way it touches nothing outside. It doesn't request data from a server, doesn't write the tab's title, doesn't start a clock. That purity is what makes UI = f(state) reliable: for a given state, the UI is always the same, no surprises.
But a real app has to touch the outside world. Mercado's products aren't written in the code: they come from the network. The browser tab's title —that thing that appears up top, in the browser bar— should say the name of the product you're viewing, and that's writing to the document. A chat app needs to connect to a server and keep listening. A timer that updates "3 minutes ago" needs a clock. None of that can be done by the render, because the render is pure and all of that is impure: they're side effects, effects that happen off to the side of the UI computation, over systems that live outside React.
Connection with the module. Here's the door that UI = f(state) was missing: effects. An effect —which you write with the useEffect hook— is React's mechanism for synchronizing your component with an external system. Notice the verb: synchronize, not transform. An effect doesn't compute data (that's what the render does, deriving); an effect connects the component with something outside —the network, the document, a connection, a timer— and keeps it up to date when the state changes. All the difficulty of useEffect, and almost all its bugs, come from confusing those two things. That's why the module has two faces, and both matter equally: learning to use an effect well (for what really is an external system) and learning to recognize when you don't need one (when what you were going to "sync" is actually derived in the render).
This module works on four pieces, and each one is measured by executing code in Node. The lifecycle —render (describes, pure) → commit (React paints) → effect (only then syncs)—; the dependency array —[] once, [dep] when dep changes, no array every render—; the cleanup —the function the effect returns to undo what it set up—; and data fetching with its two classic traps —the race condition and the double effect of StrictMode—. And a whole lesson dedicated to the other face: when you don't need an effect. As throughout the course, the real JSX syntax with useEffect is shown in the blocks —it's the exact API you'll write— and the pure logic of the cycle is executed in Node, with a manual deterministic queue (no real setTimeout), so each output is reproducible letter by letter.
An analogy: the appliance and the outlet
Imagine you enter a room and want to use a fan. You do two things: on entering, you plug it in (it starts spinning); and on leaving, you unplug it (it stops spinning and stops using electricity). Those two actions are inseparable: if you plug it in and never unplug it, the fan keeps spinning in the empty room, burning electricity for no one. And if you enter another room, first you unplug the one here and then you plug in the one there; you never leave two fans spinning at once out of carelessness.
An effect in React is exactly that pair of actions. The setup —the effect's body— is plugging the appliance into the outlet: opening the connection, subscribing to the channel, starting the timer, requesting the data. The cleanup —the function the effect returns— is unplugging on leaving: closing the connection, canceling the subscription, stopping the timer. And just as on changing rooms you unplug before plugging in, when a dependency of the effect changes, React runs your cleanup (unplugs the old) and then your setup again (plugs in the new). Keep this image, because it sums up the whole module: you plug in on entering, unplug on leaving, and on changing rooms you unplug before reconnecting. The one who plugs in without unplugging leaves appliances spinning in empty rooms —in React, that's called a leak, and we'll measure it—.
There's a second analogy worth keeping at hand, for the dependency array: watering the plant when the season changes. You don't water at every instant (that would be no array, every render); you don't water only once and never again (that would be [], only on mount); you water when the season changes (that's [season]: the effect reacts to a specific change). And a third, for the cleanup: the subscription to a magazine you have to cancel when you no longer want it, or issues keep arriving (and charging you) forever. The three point to the same thing: an effect isn't an instruction that fires and is forgotten; it's a connection that's maintained and that you have to know how to release.
The case that goes with us: Mercado's storefront, now connected
We continue with the Mercado storefront —the search bar, the product list, the cart—. Up to module 5, the products were written as a fixed array in the code: the app worked, but the data was fake, hardcoded by hand. In a real store, the products come from a server. That's this module's debut: Mercado's App, on mounting, fires an effect that requests the products from the network and, when they arrive, stores them in state; the list, which started empty (Loading...), fills on its own. And there's a finer second effect: when the user types in the SearchBar and the search is done on the server (not filtering on the client, but asking the server for that query's results), each change of query fires a new request —and here appears the trap the module teaches how to solve: if two requests come back out of order, the old response can override the new one, and the cleanup is what prevents it—.
Remember the data: a product (Product) has id, name, priceCents (the price in cents, as an integer: 2599), category, and inStock. When showing it, we format it with formatPrice(2599) → "$25.99". And remember the storefront tree, because the loading effect lives at the root:
flowchart TD
App["App<br/>[effect: load products on mount]"] --> SearchBar[SearchBar]
App --> ProductList[ProductList]
App --> Cart[Cart]
ProductList --> PC1[ProductCard]
ProductList --> PC2[ProductCard]
Net["Network / server<br/>(external system)"] -. response .-> App
App -. fetchProducts .-> Net
The dotted arrows to the Network are the module's novelty: the App leaves React to talk to an external system. The render can't do that; an effect does.
Worked example: the render → commit → effect cycle
Before anything you have to see when an effect runs within a component's life, because everything else comes from there. The sequence has three moments and a fixed order:
- Render. React runs your component (a function). The component describes how the UI should look. It's pure: it doesn't touch the DOM, doesn't request data, it only returns the description.
- Commit. React takes that description and writes the DOM: the screen now looks a certain way.
- Effect. After the screen painted, React runs your effects. Only here does the component synchronize with the external world.
The key point —the one that resolves half the confusion with useEffect— is that the effect runs after the commit, not during the render. When your effect runs, the DOM already exists and the screen already painted. Let's see it with a minimal component: a product page that, on mounting, synchronizes the tab's title (document.title) with the product's name. First, the component as you'll write it in React:
import { useEffect, useState } from 'react';
function ProductPage() {
const [productName] = useState('Wireless Mouse');
useEffect(() => {
document.title = productName; // syncs an external system: the document
});
return <h1>{productName}</h1>;
}
Read it with the analogy: document.title = productName is plugging the appliance in —touching something that lives outside React (the browser's document)—. And notice that it's not in the body of the function that returns the <h1>; it's inside useEffect, set apart, because the render must stay pure. The <h1> is the description (render); the document.title is the effect (synchronization).
Now, how do we execute it if there's no browser? With the usual trick: we model the three phases as functions that print their turn, and verify the order. The render describes (and schedules the effect), the commit "paints", and only after does the effect run:
// Mini runtime: one state cell + one effect slot.
// Models the cycle: render (describes, PURE) -> commit (paints the DOM) -> effect (syncs).
let stateCell;
let firstRender = true;
let effectSlot; // { deps, cleanup }
let pendingEffect = null; // the effect scheduled to run AFTER the commit
function useState(initial) {
if (firstRender) stateCell = initial;
return [stateCell, (next) => { stateCell = next; renderAndCommit(); }];
}
function useEffect(setup) {
if (effectSlot === undefined) {
effectSlot = { cleanup: undefined };
pendingEffect = setup; // does NOT run now: scheduled for after the commit
}
}
// The component: on mount, syncs document.title with the product's name.
function ProductPage() {
const [productName] = useState('Wireless Mouse');
console.log(` [render] describing the UI: <h1>${productName}</h1> (pure function, does NOT touch the DOM)`);
useEffect(() => {
console.log(` [effect] syncing with outside: document.title = "${productName}"`);
});
return { productName };
}
function renderAndCommit() {
ProductPage(); // 1) RENDER: describes and schedules the effect
firstRender = false;
console.log(' [commit] React writes the DOM: the screen already shows the <h1>'); // 2) COMMIT
if (pendingEffect) { // 3) EFFECT: only after painting
const cleanup = pendingEffect();
effectSlot.cleanup = typeof cleanup === 'function' ? cleanup : undefined;
pendingEffect = null;
}
}
console.log('=== Lifecycle: render -> commit -> effect ===\n');
renderAndCommit();
What to expect. When you run the file with Node, the output is exactly this:
=== Lifecycle: render -> commit -> effect ===
[render] describing the UI: <h1>Wireless Mouse</h1> (pure function, does NOT touch the DOM)
[commit] React writes the DOM: the screen already shows the <h1>
[effect] syncing with outside: document.title = "Wireless Mouse"
Read the three lines in order, because that order is the lesson. First [render]: the component describes its UI —the <h1>— and touches nothing outside. In the middle of that, the call to useEffect didn't execute the effect; it only scheduled it (that's why the document.title doesn't appear yet). Then [commit]: React writes the DOM, the screen now looks a certain way. And at the end, [effect]: only with the DOM already painted does the effect run and sync the tab's title. The effect is last, not first. If your effect needed to measure the width of a DOM element, it would work, because the DOM already exists when the effect runs. And if by mistake you had written document.title = ... directly in the render body (outside useEffect), you'd have broken purity: the render would touch the external world, and React doesn't guarantee how many times nor when it runs a render. That's why side effects go in useEffect, set apart, and run in their turn: after the commit.
The module map
Keep this route; it's how each lesson builds a part of "effects":
Topic Lesson Key idea
──────────────────────────────── ──────── ──────────────────────────────────────────
What a side effect is L2 the effect SYNCS with outside (network, DOM,
connection); the render DESCRIBES and is pure
The dependency array L3 [] once; [dep] when dep changes;
no array every render (Object.is)
The cleanup L4 the function the effect RETURNS; runs
before the next setup and on unmount
Data fetching L5 request on mount; products null -> data;
the effect is the place (the network is external)
Race, cleanup and StrictMode L6 out-of-order responses -> ignore; the
dev double effect; why React Query exists
You might not need it L7 if it's computed, DERIVE in the render; don't
sync state with state via an effect
──────────────────────────────── ──────── ──────────────────────────────────────────
Load Mercado's products L8 the mini-project, executed
The boundary: what does NOT enter this module
Knowing the boundary saves you from expecting things that come in another guide —and avoids the error of using useEffect for something that already has a better tool—.
- Data fetching on the server / SSR is the Next.js guide (
nextjs-app-router). In this module we request data from the client, with an effect, and we simulate the network. In a production app, a good part of the data is requested on the server —before the page reaches the browser—, and that changes the game: nouseEffect, no loading state, no race condition. That's Server Components and server-side data fetching, and it's from the Next.js guide. Here you see the raw client-sideuseEffect, which is what you need to understand first. - Data libraries —React Query, SWR— (cache, deduplication, mutations, revalidation) are the state and data guide (
frontend-state-and-data). In lesson 6 you're going to suffer first-hand the traps of raw fetching (the race condition, the double effect, handling loading and error by hand). Those libraries exist precisely so you don't have to write all that withuseEffect. Here you'll see the problem they solve; the solution, with its API, is the other guide. - Lifting the state and shared state is module 7. In the project, the
Appowns the loadedproducts; how they come down to the children and how thecartis shared betweenProductCardandCartis lifting the state. - And everything from the ecosystem that the previous modules already delimited stays the same: HTML/CSS in depth →
web-fundamentals-html-css; styles/design systems →ui-systems-and-design-implementation; performance and deploy →fullstack-performance-and-deployment.
Common mistakes
Putting a side effect directly in the render body. What happens: someone writes document.title = productName or does a loose fetch, in the component's body, outside useEffect. Why it happens: it seems more direct —"I put it where I need it"—. How to spot it: the effect runs at strange moments (twice, or on every render), the tab flickers, or the network receives extra requests; and if you touch the DOM, sometimes the element doesn't exist yet. How to fix it: everything that touches the external world goes inside useEffect, so it runs in its turn (after the commit) and doesn't break the render's purity. The render describes; the effect acts.
Believing that useEffect is for transforming data. What happens: you use an effect to compute the filtered list, the cart total, or the full name from two fields —and store it in another state—. Why it happens: you confuse "synchronize" with "compute". How to spot it: you have a useEffect that only reads state, transforms it, and calls a setState; there are two pieces of state where one is always a function of the other. How to fix it: that's not an effect, it's a derived —it's computed in the render (module 5)—. Lesson 7 measures it: the transforming effect causes extra renders and desyncing bugs. Rule: if there's no external system in between, it's probably not an effect.
Forgetting that the effect runs after painting (expecting it to run "during" the render). What happens: someone puts inside the effect something they needed before the first paint (to avoid a flicker), and sees the UI show for an instant with the old value. Why it happens: you think of the effect as part of the render. How to spot it: a momentary flash (the tab says "React App" and then changes to the real name). How to fix it: accept the order —render → commit → effect— as it is; for most synchronizations (title, analytics, connections) it's perfectly fine for it to happen after painting. The cases that really need to run before the paint have another tool, mentioned in the docs, that the later guides cover.
Exercises
Exercise 1 — Effect or not effect? For each situation, say whether it needs a useEffect (because it touches an external system) or whether it doesn't need one (because it can be derived in the render or goes in an event handler): (a) setting the tab title with the name of the product being viewed; (b) computing the cart total by summing its items' prices; (c) sending the order to the server when the user clicks "Checkout"; (d) connecting to a chat server while the component is mounted; (e) showing the product list filtered by the query.
See solution
- (a) Effect. The tab title is the
document.title, a system external to React. It goes in auseEffectthat syncsdocument.titlewith the product's name. (Lesson 2.) - (b) Not an effect — derive. The total is a transformation of the cart items:
items.reduce(...). It's computed in the render; there's nothing external. Storing it in state and syncing it with an effect is lesson 7's anti-pattern. - (c) Not an effect — event handler. "When the user clicks" describes a user action, not a continuous synchronization. The submission goes in the button's
onClick(module 4), not in an effect. An effect is for what should happen because the component is on screen, not because the user did something. - (d) Effect. Connecting to a chat server is synchronizing with an external system that must be active while the component lives: setup = connect, cleanup = disconnect. The textbook case of
useEffectwith cleanup. (Lessons 2 and 4.) - (e) Not an effect — derive. The filtered list is computed from
productsandqueryin the render (products.filter(...), module 5). There's no external system; it doesn't go in an effect.
The rule that separates them: is there an external system (network, DOM, connection, timer)? If yes, and it must be maintained while the component lives, it's an effect. If it's a computation, derive. If it's a response to an action, it's a handler.
Exercise 2 — The order of the cycle. With the analogy of the appliance and the outlet, explain why the effect runs after the commit (after the screen painted) and not during the render. What problem would touching the document or requesting data during the render bring?
See solution
The render is like deciding which appliance goes in the room: you describe the UI, but you don't connect anything yet. The commit is setting up the room as you described it (React writes the DOM, the screen looks a certain way). And only then, with the room already built, you plug the appliance into the outlet (the effect syncs with outside). It has to be in that order because the effect sometimes needs the "room" to already exist: if the effect measures the width of a DOM element, that element must be painted, and it only is after the commit.
Touching the external world during the render brings two problems. First, it breaks purity: the render must be able to run however many times React wants, without visible consequences; if in the middle it requests data or writes the document, each render would fire loose requests or changes. Second, the DOM doesn't exist yet during the render, so any effect that depends on the DOM would fail. That's why React sets side effects apart in useEffect and runs them in their turn: at the end, after painting.
Exercise 3 — Name the setup and the cleanup. For a chat app that connects to a server while the component is mounted, describe (in words, no code) what the effect's setup should do and what the cleanup should do. Then say what would happen if the effect had no cleanup and the user navigated among five different chat rooms.
See solution
- Setup (plug the appliance in): open the connection to the current room's chat server and start listening for messages.
- Cleanup (unplug on leaving): close that connection, stop listening. It's the function the effect returns.
If the effect had no cleanup and the user went through five rooms, each room change would open a new connection without closing the previous one. In the end there would be five open connections at once —five appliances spinning in rooms you already left—: the component would receive messages from old rooms, use up resources, and could show mixed data. That's a leak, and it's exactly what the cleanup prevents: on changing rooms, first it disconnects the previous one (cleanup) and then connects the new one (setup), always leaving exactly one active connection. We'll measure it in lesson 4.
Summary and next step
In this lesson you installed the last big piece of UI = f(state): effects. You saw that the render is a pure function that doesn't touch the external world, and that's why React needs a separate door —useEffect— to synchronize the component with external systems: the network, the document, a connection, a timer. The key word is synchronize, not transform: the render derives the data (module 5), the effect reaches what's outside. You measured it by executing the lifecycle —render → commit → effect— and verified the effect runs last, after the screen already painted. And you kept the analogy that sums up the module: an effect is plugging an appliance into the outlet on entering and unplugging it on leaving —setup and cleanup, inseparable—.
Before moving on you should be able to: define a side effect and explain why the pure render can't have any; distinguish "synchronize" (effect) from "transform" (derive); name the order render → commit → effect and why the effect goes last; and locate the boundary —what belongs to this module (raw client useEffect) and what belongs to other guides (SSR in nextjs, React Query in frontend-state-and-data)—.
Lesson 2 takes the first piece of the map and nails it: what a side effect is, with the operational definition and the order of the cycle measured in detail. There you'll see, executed, why an effect that writes to the document is different from a computation that derives a piece of data —and why confusing them is the number-one source of bugs with useEffect—.
Resources
- React, "Synchronizing with Effects" — react.dev/learn/synchronizing-with-effects. The official page that introduces
useEffectas a tool to synchronize with external systems; the starting point and the thesis of this module. In English. - React, "Adding Interactivity" — react.dev/learn/adding-interactivity. The index of the section where all this lives (state, events, effects); useful to see where the module fits. In English.
- React, "You Might Not Need an Effect" — react.dev/learn/you-might-not-need-an-effect. The other face of the module (lesson 7): when not to use an effect. Read it soon: it avoids half the badly written
useEffects. In English. - MDN, "Document: title property" — developer.mozilla.org/en-US/docs/Web/API/Document/title. The
document.titlewe sync in the example: an external browser system, underneath React. In English.