EverProduct
Frontend

Stage 04 · The Application

One Source of Truth: State

The screen says three items; the list shows four. Nothing is broken — the number was stored separately from the list, and two copies of a fact will always find a way to disagree.

A cart shows "3 items" above a list of four. Somewhere a delete removed the row and forgot to decrement the counter, or added one and updated only the total.

The bug isn't the missing line of code. The bug is that the same fact was stored twice. As long as it is, some path through your application will update one copy and not the other, and no amount of care prevents it — you're maintaining an agreement by hand, forever.

If it can be computed from something else, it isn't state — it's a second copy waiting to disagree.

State is the minimum

Here's the definition worth carrying: state is the smallest set of values from which the entire screen can be derived.

Everything else is a calculation. The item count is the list's length. The total is a sum. The "no results" message is whether the filtered array is empty. Whether the submit button is enabled is a function of the fields. None of those are state, and storing them is how screens start lying.

So the question to ask before adding anything is: can this be computed from what I already have? If yes, compute it at render time. It'll be correct by construction, forever, without your attention.

The genuine exceptions are rare and identifiable: a value expensive enough that recomputing it is measurable, or something you deliberately want to freeze at a moment in time.

Where it lives

Once you know what the state is, the second decision is where it goes: at the lowest component that contains everyone who needs it.

Too low and you can't share it, which produces the classic move of lifting state up to the common parent. Too high — the reflex of putting everything at the top "just in case" — is the more expensive mistake, because now unrelated parts of your application are coupled through one object, everything re-renders on every change, and nobody can tell which component actually cares about which field.

The default should be as local as possible, moved up only when a second component genuinely needs it.

The five kinds

Most state confusion clears up the moment you notice that "state" is five different things with different rules. Beginners put all five in one place — usually a global store — and then wonder why everything is entangled.

1. Local UI state. Is the dropdown open, which tab is active, what's typed in a field so far. It belongs inside the component and nowhere else. Most state is this, and most of it never needs to travel.

2. Shared client state. Theme, the signed-in user, the contents of a cart. Genuinely global, genuinely small. This is what context or a store is for — and it should stay a short list.

3. Server state. Data fetched from an API — and this is the reframing that changes how people write applications: it isn't your state, it's a cache of someone else's. It can be stale. It can be refetched. Two components asking for the same thing shouldn't produce two requests. It needs loading and error states, retries, invalidation after a mutation.

Treating it like local state is what produces hand-rolled loading flags in every component, duplicate requests, and data that stays wrong until a reload. This is exactly what a data-fetching library does for you — caching, deduplication, revalidation, stale-while-revalidate — and it's the single highest-value library category in frontend. Understand the problem first; then the library stops looking like extra machinery and starts looking like the obvious answer.

4. URL state. The current filters, the search query, the page number, the open tab. This is the most under-used location in frontend, and the test is one question: should a person be able to copy the link and get this same view? If yes — and for filters and search the answer is almost always yes — it belongs in the URL. You get sharing, refresh survival, and working back and forward buttons for free, all of which are otherwise bugs users report.

5. Form state. Draft values, touched fields, validation errors. Local by nature, and covered in the forms article.

Replace, don't mutate

The rule you'll hear everywhere, now with its reason: frameworks decide what changed by comparing references. Mutate an array in place and the reference is identical, so as far as the framework is concerned nothing happened — and your screen keeps showing the old data while your data is new.

So updates produce new values rather than editing old ones: a new array from a map or filter, a new object built with spread. For deeply nested state this becomes awkward, which is itself a signal — deeply nested state is usually a sign the shape is wrong, and flattening it is a better fix than a more elaborate copy.

Effects, and the mistake everyone makes

An effect exists to synchronise your component with something outside the framework: a subscription, a timer, a browser API, a manual event listener, a third-party widget.

That's the list. The overwhelmingly common misuse is using an effect to update state when other state changes — recomputing a derived value, resetting a field when a prop changes, syncing two pieces of state. Every one of those produces an extra render, a moment where the screen shows an inconsistent intermediate, and a dependency array that becomes a source of infinite loops.

The rule: if it can be computed during render, compute it during render. Effects are for talking to the outside world. If you're writing one whose only job is to keep two pieces of your own state in agreement, you had one piece of state and a calculation, and storing the second copy is the actual bug — the same disease as the counter at the top of this article.

And whatever an effect sets up, it must tear down: the listener, the timer, the subscription, the in-flight request. That cleanup is not optional politeness; it's the difference between an application that stays usable for an hour and one that doesn't.

Make illegal states impossible

A pattern that removes bugs rather than fixing them.

Three booleans — isLoading, hasError, isEmpty — allow eight combinations, of which five are nonsense: loading and error, error and empty. Nothing stops those combinations existing, and eventually one does, producing a screen nobody designed.

Replace them with one value that can be exactly one of loading, error, empty or success, and the nonsense becomes unrepresentable. Your rendering turns into a single choice between four cases, and you'll notice you were never handling one of them.

This idea gets much stronger with types, which is the next article, but it's worth adopting now: fewer variables with more meaning beats more variables with less.

Re-rendering, in the right order of worry

A state change re-runs the component and the framework applies the differences. Beginners hear "re-render" and imagine the whole page being rebuilt; it isn't, and the framework is fast.

Which is why the correct order is: write it clearly, measure if it feels slow, optimise the specific thing you measured. Memoising everything preemptively adds complexity, adds its own cost, and usually targets the wrong component — and it makes code harder to read in exchange for a benefit nobody verified. The performance article covers how to measure. Until then, clarity wins.

In practice

List the state before writing the component. Literally, in a comment or on paper: what does this screen need to know? Then cross off everything that can be computed. What survives is your state, and it's usually half of what you'd have written.

Hunt for second copies. In your own project, find a value stored in two places. Delete one and compute it. This is the single highest-yield refactor in this article.

Move one thing into the URL. Filters or a search query. Then reload the page and press back. The improvement is immediate and users notice it.

Audit your effects. For each one, ask what outside thing it synchronises with. If the answer is "nothing, it just updates other state", remove it.

Ask "what changed?" out loud when a screen is wrong. State, or the derivation from it? That question splits every state bug in two and is faster than reading code.

Explain your state model to someone. The How to Learn sphere's point applies with unusual force here: in your head this is a draft where a feeling of coherence stands in for real links, and the missing link surfaces in the sentence where you try to say who owns what.

Check yourself

Close the article and answer in your own words:

  1. What's the definition of state, and what test removes something from the list?
  2. Why is putting all state at the top of the tree a mistake?
  3. Name the five kinds of state and where each belongs.
  4. Why is server data a cache rather than state, and what follows from that?
  5. What's the test for whether something belongs in the URL?
  6. Why does mutating an array fail to update the screen?
  7. What are effects for, and what's the most common misuse?

In short

  • Two copies of one fact will eventually disagree; state is the minimum set of values everything else can be derived from.
  • Derive at render time instead of storing, and keep state at the lowest component that contains everyone who needs it.
  • Five kinds with different homes: local UI, shared client, server cache, URL, and form state.
  • Server data is a cache — it goes stale, needs deduplication and revalidation, and is what data-fetching libraries exist for.
  • If a link should reproduce the view, the value belongs in the URL: sharing, refresh and the back button come free.
  • Replace values rather than mutating them, because change is detected by reference.
  • Effects synchronise with the outside world and must clean up; using one to keep your own state in sync means you stored something derivable.
  • Collapse boolean combinations into a single status so illegal states can't be represented, and optimise re-rendering only after measuring.