A session that fills in a lot of gaps. Some advanced Promise patterns, a React performance secret most beginners don't know about, and then a mental model for HTTP and APIs that's worth reading twice.
.then() and .finally() Are Full Functions — Use Them That Way
A common mistake is treating .then() like it can only do one thing. It's a full function — you can declare variables, write logic, run conditions, all of it:
Promise.allSettled([fetchConfig, fetchColor])
.then((results) => {
const configResult = results[0].status === 'fulfilled' ? results[0].value : defaultConfig;
const colorResult = results[1].status === 'fulfilled' ? results[1].value : '#FFFFFF';
})
Promise.allSettled() waits for all promises to finish — whether they resolved or rejected — and gives you a results array where each item tells you the status and value. No promise getting rejected silently breaks everything else. You handle each one individually.
The Loader and allSettled Are Best Friends
Think about it — a loading spinner should disappear exactly when the data is ready. Not before, not after. Promise.allSettled() + .finally() is the cleanest way to guarantee that:
Promise.allSettled([fetchConfig, fetchColor])
.then((results) => {
// handle your data
})
.finally(() => {
setLoading(false);
})
.finally() runs no matter what — success, failure, doesn't matter. The spinner dies exactly when the promises settle. If you don't use .finally(), you risk the spinner spinning forever if an error is thrown and .then() never runs.
JSON.stringify(data, null, 2) — Pretty Printing JSON
JSON.stringify(data.config, null, 2)
The 2 here is the indentation level — it formats the output with two spaces per level so it's actually readable in a console or log. Without it you get one long unreadable line. The null in the middle is the replacer argument — passing null just means "include everything, no filtering."
AbortController — Cancel a Request When You No Longer Need It
Imagine a user opens a page, a fetch starts, then they immediately navigate away. The fetch is still running in the background — and when it finishes, it might try to update state on a component that no longer exists. AbortController stops that:
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => setData(data));
// When the user leaves:
controller.abort();
Inside useEffect, you'd call controller.abort() in the cleanup function — the one that runs when the component unmounts. Only update state if the user is still on the page. Clean, safe, no ghost updates.
React's Automatic Batching — One Render, Not Two
In React 18+, when you call multiple state setters one after another inside a .then() block:
setData(result);
setLoading(false);
React doesn't re-render twice. It waits until the function finishes, collects all the state updates, and does one single render with everything applied at once. This is called Automatic Batching and it's a silent performance optimisation you get for free.
Why it matters — if setLoading(false) runs in the wrong place, React might render once with new data but loading still true, then render again with loading false. The user sees a half-finished page for a split second — that flicker. Keeping your state updates together inside .then() or .finally() lets React batch them properly and render once cleanly.
useState Always Holds the Initial Value
const [data, setData] = useState(null);
Whatever you pass into useState() is the starting value — what the component holds on its very first render before anything updates it. After that, only setData() changes it. Simple but worth keeping clear in your head especially when debugging why something is null on first render.
How the Web Actually Works — The Full Mental Model
These are the rules. Read them slowly:
API does not travel — the API is just a definition. A menu of what's available.
HTTP request travels — the actual message that goes across the internet.
Frontend never talks directly to DB — the browser has no business touching a database directly.
Backend owns DB communication — the server talks to the database, nobody else.
Middleware enables body parsing — without it, your backend can't read the body of incoming requests.
Methods express intent — the HTTP method tells the server what you want to do, not just where to go.
The HTTP Methods — What Each One Means:
GET — "please send me data"
POST — "please create something new"
PUT — "please replace this entirely"
PATCH — "please update just part of this"
DELETE — "please remove this"
The Restaurant Mental Model — Stick This in Your Head:
Menu = API — defines what can be ordered, doesn't deliver anything itself
Order = HTTP request — the actual message sent
*Waiter *= Internet — carries the message back and forth
*Kitchen *= Backend — receives the order, does the work
*Food *= Response — what comes back
So when someone says "the API sends data" — not quite. The API defines what data can be requested. The HTTP request is what actually goes out and asks for it. The API is the menu. The request is the order. The backend is the kitchen that actually makes the food.
Top comments (0)