Without notes, state yesterday’s main idea and one unresolved question.
Effects and external synchronization
React Frontend Fundamentals
Objective
Use effects only for synchronization with external systems.
A React component is a reusable functional block with inputs (props), internal state, and rendered output.
- render versus effect
- dependencies
- cleanup
Why this matters
Everything so far has been self-contained: data lives in a file, the user changes it, React redraws.
Real screens get their data from somewhere else. Today you learn useEffect, the one hook that
reaches outside React — and, just as importantly, the rules that stop it becoming the tool people
reach for by reflex and regret.
By the end of the hour your equipment list loads from a URL and handles all four things that can happen: still loading, loaded with data, loaded with nothing, and failed.
Render versus effect
A component's job during render is to compute UI from props and state and return it. That computation must be pure: same inputs, same output, no observable side effects. React relies on this — it may call your function more than once, and it decides when.
So these are not allowed during render:
function EquipmentList() {
fetch("/equipment.json"); // wrong: fires on every render
document.title = "Equipment"; // wrong: touches something outside React
setCount(count + 1); // wrong: infinite loop
return <ul>...</ul>;
}
Most side effects belong in event handlers — the user clicked, so send the request. That covers more cases than beginners expect. But some work is not caused by any event; it is caused by the component being on screen at all. "While this list is displayed, it should show what the server has." That is synchronization with an external system, and it is what effects are for.
An external system means anything React does not control: the network, document.title, a
setInterval timer, localStorage, a browser API, a non-React library.
useEffect(() => {
document.title = `${items.length} items`;
}, [items.length]);
React runs that function after it has rendered and committed the result to the DOM. Two arguments: the setup function, and the dependency array.
Combinational logic and the output driver
Render is combinational logic: inputs in, outputs out, no memory of being evaluated and safe to evaluate as often as you like. An effect is the output driver stage that actually energizes something external — a relay, a display, a comms link — once the logic has settled. You do not put the relay coil in the middle of the logic. You settle the logic first, then drive the output.
Dependencies
The second argument tells React when to run the setup again. There are three cases, and they are easy to mix up:
| You write | React runs the effect |
|---|---|
useEffect(fn) |
after every render |
useEffect(fn, []) |
after the first render only |
useEffect(fn, [a, b]) |
after the first render, and whenever a or b changed |
"Changed" means React compared the value with the previous render's using Object.is — the same
comparison as state. Which is why a dependency that is a new object or array literal every render
counts as changed every render, and turns [options] into no dependency array at all.
The dependency list is not a preference; it is a claim that the effect uses exactly these values
from the component. If you read equipmentId inside the effect, it must be in the list, or your
effect will keep using an old id after the prop changes. React's ESLint rule for hooks checks this
for you — do not silence it, fix the code.
The infinite loop
An effect that sets state which is also in its own dependency list re-triggers itself forever:
render → effect → set state → render → effect. The same happens with useEffect(fn) and no array
if fn sets state. Symptoms: the dev server pegs a CPU core and the tab freezes. Fix it by
narrowing the dependencies, or by asking whether the value should be state at all — most of these
are Day 59's derived values in disguise.
Cleanup
An effect can return a function. React calls it before running the setup again, and once more when the component is removed from the screen. That is cleanup, and it is what makes effects safe to repeat.
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id);
}, []);
Without the clearInterval, every mount leaves a timer running forever. The pattern is universal:
whatever the setup starts — a timer, a subscription, a socket, a listener — the cleanup stops.
React helps you notice a missing cleanup. In development, <StrictMode> (which the Vite template
put in main.tsx on Day 57) deliberately mounts each component, unmounts it, and mounts it again.
So you will see effects run twice and requests fire twice in the console. That is not a bug and it
does not happen in production builds; it is React proving that your effect survives being run
twice. If it does not, you have a real bug that would have shown up later.
Leaving the tap running
Every time you open the tap you must close it. If the setup runs again while the first tap is still open, water goes everywhere and nobody can tell which tap is responsible. Cleanup is closing the tap you personally opened, before anyone opens another.
Stale responses
Network requests come back out of order. Suppose an effect refetches whenever equipmentId
changes: the user clicks item 1, then quickly item 2. Request 2 may answer first, then request 1
arrives and overwrites the screen with the wrong item's data. Nothing threw an error; the UI is
just wrong.
The fix uses cleanup. A local flag says "this effect run is no longer current", and cleanup sets it:
useEffect(() => {
let ignore = false;
async function load() {
const data = await getEquipment(equipmentId);
if (!ignore) setItems(data);
}
load();
return () => { ignore = true; };
}, [equipmentId]);
When equipmentId changes, React runs the cleanup for the old run first, setting its ignore to
true. That run's response is then discarded whenever it lands. Each effect run has its own
ignore variable, which is why this works.
Note the shape: the effect's setup function itself is not async. It must return either
nothing or a cleanup function, and an async function always returns a promise. So you declare an
async function inside and call it.
Walkthrough
Give yourself something to fetch. Create public/equipment.json in your project — Vite serves
everything in public/ from the site root, so it is reachable at /equipment.json:
[
{ "id": "PUMP-3", "name": "Coolant pump 3", "location": "Bay A", "status": "needs-service" },
{ "id": "MTR-1", "name": "Conveyor motor 1", "location": "Bay B", "status": "ok" }
]
Now load it in src/components/EquipmentScreen.tsx:
import { useEffect, useState } from "react";
import type { Equipment } from "../types";
export function EquipmentScreen() {
const [items, setItems] = useState<Equipment[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let ignore = false;
async function load() {
setIsLoading(true);
setError(null);
try {
const response = await fetch("/equipment.json");
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const data: Equipment[] = await response.json();
if (!ignore) setItems(data);
} catch (caught) {
if (!ignore) setError(caught instanceof Error ? caught.message : "Unknown error");
} finally {
if (!ignore) setIsLoading(false);
}
}
load();
return () => { ignore = true; };
}, []);
if (isLoading) return <p role="status">Loading equipment…</p>;
if (error) return <EmptyState message={`Could not load equipment. ${error}`} />;
if (items.length === 0) return <EmptyState message="No equipment recorded yet." />;
return <EquipmentList items={items} />;
}
Three details that are easy to get wrong. fetch only rejects when the request could not be made
at all — a 404 or a 500 arrives as a perfectly successful promise, which is why you must check
response.ok yourself (Day 36's status codes, still true). caught instanceof Error is Day 32
narrowing: a caught value is typed unknown, so you cannot read .message off it directly. And
role="status" makes the loading line an announcement, so a screen reader user hears that
something is happening.
Now break it on purpose: change the URL to /equipmnt.json and reload. Vite's dev server answers a
missing path with the app's HTML, so response.ok is true and response.json() throws while
parsing. You will see a message like:
Could not load equipment. Unexpected token '<', "<!doctype "... is not valid JSON
That is a real, useful failure surface — the user sees a sentence instead of a blank screen.
Checkpoint
With DevTools open on the Network tab (Day 6), reload and watch one request for
equipment.json. In development you may see it twice because of StrictMode. Say out loud why
that is expected and why it will not happen in the production build.
Your turn
Deliverable: a fetch flow that does not loop and cancels or ignores stale work appropriately.
- Create
public/equipment.jsonwith at least four items matching yourEquipmenttype. - Build
EquipmentScreenas in the walkthrough, and render it fromApp.tsx. - Confirm all four states by forcing each one:
- loading — in DevTools Network, set throttling to "Slow 3G" and reload;
- success — normal reload;
- empty — temporarily change the JSON file to
[]; - error — temporarily point
fetchat a wrong path. Record what the screen said in each case.
- Add a "Reload" button that re-runs the fetch. Do this by adding a
const [reloadKey, setReloadKey] = useState(0), puttingreloadKeyin the dependency array, and having the button callsetReloadKey((n) => n + 1). - Prove the dependency rule: temporarily remove
reloadKeyfrom the array and confirm the button stops working. Put it back. - Audit every
useEffectyou now have. For each, write one sentence naming the external system it synchronizes with. Any effect that cannot answer should be a derived value or an event handler instead.
You are done when
All four states are reachable and readable, the Network tab shows one request per load rather than a stream of them, and every effect can name its external system.
Reviewer mode — after all four states work
Today's mode is reviewer: bring working code and ask for specific, actionable findings backed by evidence from your files — not general praise, and not a complete rewrite.
"Review every useEffect. Ask whether it synchronizes with an external system or hides derived state."
Check each finding against the code yourself before touching anything. An applied review you did not judge is just a rewrite you did not read.
Common pitfalls
- Making the effect function
async.useEffect(async () => {...})returns a promise where React expects a cleanup function. Declare an inner async function and call it. - Assuming
fetchthrows on 404. It resolves. Without aresponse.okcheck you try to parse an error page as JSON and get a confusing syntax error instead of a status. - Fetching in an effect with no dependency array. Every response sets state, which renders, which fetches again. The tab locks up.
- Blaming StrictMode. Doubled effects in development are intentional. If doubling breaks
something, the cleanup is missing — fix that rather than removing
StrictMode.
Verify it yourself
Open today's reference, React's Learn section, and read Synchronizing with Effects and You Might Not Need an Effect.
- You Might Not Need an Effect lists cases where people use an effect wrongly. Find two, and write what to do instead. At least one should already apply to code you wrote this week.
- Find React's own explanation of why development remounts components. Compare its wording with this lesson's claim that it is there to expose missing cleanup.
Write both into notes/day-61.md. Effects are the hook that most often gets overused; knowing when
not to reach for one is the actual skill.
The hour
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Load equipment from a mock endpoint and handle loading, success, empty, and error states.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
A fetch flow that does not loop and cancels or ignores stale work appropriately.
Working with AI today
Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.
Review every useEffect. Ask whether it synchronizes with an external system or hides derived state.
References
End-of-day quiz
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.