Lesson 06 / 22
State Types
Classifying state by kind — local, shared, URL, and server state, along with derived values; the decision procedure that places each kind, the single source of truth principle, and the cost of misplacement.
Contents
Routing is complete: the address decides which screen appears, who it is open to, and which code downloads. One kind of state the address carries has also been defined — filter, sort order, page. The rest of the North Slope Measurement Station application’s state is scattered: session information sits in guards, the station list sits somewhere else, open panels sit inside components, and measurements from the server sit in yet another place.
Calling all of this “state” hides the most important difference among them. Some are values the application itself produces and solely owns; some are copies of a remote source. This lesson separates state into kinds, gives the criterion that decides where each kind belongs, and shows the cost of misplacing it.
Five Kinds
Local state is a value a single component reads and writes: whether a dropdown is open, a text box’s current content. It is born with the component and dies with it.
Shared state is a value multiple components read, produced by the application itself: the interface theme, whether the side navigation is open, the selected unit system. Its owner is the application; nothing else changes it.
URL state is state carried in the address. The previous topic defined it: state expected to return when shared, bookmarked, and returned to with the back button.
Server state is the client-side copy of a remote source: the station list, measurement records, the signed-in user’s information. What sets it apart from the other four is that ownership does not sit with the client.
Derived value, however, is not actually state. Anything computable from other state falls into this class: the length of a filtered list, the total of selected items, a flag showing whether a request is still in flight.
This set of five can be turned into a decision procedure.
// state-types.mjs — decision procedure that places state and the cost of storing a derived value function classify(item) { if (item.derivedFrom) return ["derived", `computed from ${item.derivedFrom}`]; if (item.source === "server") return ["server", "owner is remote, a copy is kept"]; if (item.inUrl) return ["URL", "must return when shared"]; if (item.consumers > 1) return ["shared", `${item.consumers} separate consumers read it`]; return ["local", "a single component reads it"]; } const ITEMS = [ { name: "station list", source: "server", consumers: 2 }, { name: "measurement history page no", inUrl: true, consumers: 2 }, { name: "selected measurement type filter", inUrl: true, consumers: 3 }, { name: "signed-in user", source: "server", consumers: 6 }, { name: "side nav open", consumers: 2 }, { name: "interface theme", consumers: 5 }, { name: "form field current value", consumers: 1 }, { name: "dragged slider live value", consumers: 1 }, { name: "filtered measurement count", derivedFrom: "measurements + filter" }, { name: "loading indicator", derivedFrom: "server state" }, ]; console.log("-- placement --"); for (const item of ITEMS) { const [kind, reason] = classify(item); console.log(item.name.padEnd(38), kind.padEnd(12), reason); } // Storing a derived value versus computing it on every read. const filterBy = (measurements, type) => measurements.filter((m) => type === "all" || m.type === type); let state = { measurements: [{ type: "temperature", v: -4.2 }, { type: "humidity", v: 71 }, { type: "temperature", v: -3.8 }], filter: "temperature", storedCount: 2, // derived value updated manually }; const printLine = (label) => console.log( label.padEnd(34), "stored:", String(state.storedCount).padEnd(3), "computed:", filterBy(state.measurements, state.filter).length); console.log("-- derived value --"); printLine("start"); state = { ...state, measurements: [...state.measurements, { type: "temperature", v: -5.1 }] }; printLine("new temperature reading added"); state = { ...state, filter: "humidity" }; printLine("filter switched to humidity"); state = { ...state, storedCount: filterBy(state.measurements, state.filter).length }; printLine("stored count corrected manually"); state = { ...state, measurements: state.measurements.filter((m) => m.type !== "humidity") }; printLine("humidity readings removed");
-- placement -- station list server owner is remote, a copy is kept measurement history page no URL must return when shared selected measurement type filter URL must return when shared signed-in user server owner is remote, a copy is kept side nav open shared 2 separate consumers read it interface theme shared 5 separate consumers read it form field current value local a single component reads it dragged slider live value local a single component reads it filtered measurement count derived computed from measurements + filter loading indicator derived computed from server state -- derived value -- start stored: 2 computed: 2 new temperature reading added stored: 2 computed: 3 filter switched to humidity stored: 2 computed: 1 stored count corrected manually stored: 1 computed: 1 humidity readings removed stored: 1 computed: 0
The procedure’s ordering is not arbitrary. Derivability is tested first, because a derived value is not placed anywhere. Server origin comes second: the ownership question precedes the question of how many components read it. The address test comes before the sharing test, because state written to the address already becomes readable application-wide.
The Cost of Storing a Derived Value
The second part of the output walks through a single defect step by step. The filtered measurement count is stored in a state field and updated manually.
When a new measurement is added, the stored value stays at two while the computed value rises to three. When the filter switches to humidity, the gap between them reverses direction. When the stored value is corrected manually, the two agree for a moment — but the next change pulls them apart again. Every new write site is a new place where the update can be forgotten.
This is an instance of a single rule: the same information is not kept in two places. When a derived value is stored, two sources of truth appear and which one is correct becomes ambiguous. If the cost of computation is a concern, the answer is not storing it but memoizing the computation; that is the subject of the Derived Values lesson in the Component-Based Interface Development course, and the result is still a single source of truth.
Treating the loading indicator as derived rests on the same reasoning. When a separate flag is kept, the case where the request finishes and the flag is not turned off is unavoidable — on the error path, on cancellation, on a concurrent second request. The indicator’s true source is the request’s own state.
The Separateness of Server State
The other four are values the application itself produces: the written value is correct, because the application is the one writing it. This does not hold for server state. The station list on the client is a copy of the remote list at some past moment. Another user may have added a new station, someone may have corrected a measurement.
This difference raises four questions, none of which is asked for the other kinds of state. How long is the copy considered current? When it goes stale, when is it refreshed? If two components request the same data, how many requests are made? While a local change is being written to the server, what appears on screen?
Placing server state in the same location as shared state loses these four questions. The application takes the copy for the truth: data is fetched once, kept in memory, and never refreshed. The user cannot see, in the list, a station they added in another tab. For this reason server state is kept in a separate layer, with its own freshness rules; that layer is the subject of this topic’s fourth lesson.
The Placement Principle
The principle that decides where state belongs is keeping it as low as possible: a value only one component cares about is not lifted up.
The reasoning is a direct consequence of the one-way flow from the Component-Based Interface Development course. Every value lifted up creates a dependency the components in between are forced to see; it also announces its change to components it does not affect. State kept low concerns only its own component.
There is a trap in the opposite direction: keeping state lower than it needs to be. When two sibling components each keep the same value inside themselves, two copies form and drift apart. The criterion is fixed: the nearest common ancestor of the components reading the value. It cannot be kept lower than that, and keeping it higher is unnecessary.
This principle does not apply to two kinds. URL state’s location is already fixed; server state, meanwhile, does not belong to any node of the component tree, because components in different parts of the tree request the same data and the copy must be singular.
Summary
- State separates into five classes: local, shared, URL, server, and derived; the first four are stored, the fifth is computed.
- The decision procedure asks derivability first, then ownership, then the address, and consumer count last.
- When a derived value is stored, two sources of truth form and every write site risks drifting apart again; the answer is not storing it but memoizing the computation when needed.
- Server state’s owner is not the client; the questions of freshness, refresh, request repetition, and what appears on screen during a write are asked only for this kind.
- Client state is kept at the nearest common ancestor of the components that read it — keeping it lower produces copies, keeping it higher creates an unnecessary dependency.
Next Step
The kinds are separated, but how shared state is held is not yet clear. A value kept at the common ancestor gets threaded through dozens of components that do not use it as the tree deepens; worse, who changes this value and in which event scatters through the code. Selecting a row in the station list, deleting a measurement, signing out — all three change the same state from different places, and when a bug appears, who made the change cannot be traced. The next lesson covers the centralized store pattern, which collects state in a single place, accepts change only through named actions, and defines transitions with a pure function.
To keep your progress and take notes, Log in
My notes
Log in to take notes.