skip to content

In a store with unidirectional flow, what does routing a change through an action and an applier buy over direct assignment?

level: middleimportance: must knowfreq 64%

answer

  1. one direction: intent in, notification out
  2. actions name the change
  3. apply in one place, notify after
  4. deterministic applier, testable without a tree

basics

~20 s

An action names the change and carries its data, one applier turns it into the next state, and subscribers are notified afterwards. The gain is a single funnel: every change has a name, an order, and one place that enforces invariants spanning fields.

solid answer

~50 s

Unidirectional flow means changes travel one way. Something dispatches an **action** — a name plus the data the change needs — the store applies it in exactly one place, and only then are subscribers notified, each re-reading and updating if its slice moved. The applier is usually a pure function of the previous state and the action, though some stores instead let an action mutate a tracked draft; the discipline is the same. What it buys: every mutation has a name you can log, so a wrong value has a traceable cause; the write path is a function testable with plain values and no tree; invariants that span fields live in one place instead of at every call site; and subscribers see whole states, never a half-applied one, because notification follows the completed apply. The cost is ceremony — for one value with one writer, a plain setter is the honest choice.

go deeper

for a junior

Recall the order: dispatch an action, the store applies it in one place, then subscribers are notified and re-read. Know that the action describes a change and does not itself perform it.

for a middle

Explain what the funnel buys — a named and ordered change log, invariants in one place, a write path testable with plain values — and why notification after a completed apply prevents a half-applied read.

for a senior

Demonstrate the judgment about effects and reentrancy: which work must stay outside the applier, how a start/settle action pair models asynchronous change, and what a dispatch from inside a notification does.

for a principal

Argue the cost honestly. An action layer is discipline the whole codebase pays for; be able to say which stores earn it, which should keep a plain setter, and how you keep the action vocabulary from sprawling.

## The shape of the flow Unidirectional flow describes a loop with one direction of travel. Something in the tree — an event handler, an effect, another store — **dispatches an action**: a value that names the change and carries the data it needs. The store applies that action in exactly one place, producing the next state. Only then does it notify subscribers. Each subscriber re-reads, and the components whose slice moved update. Nothing writes state on the way down; nothing observes half-applied state on the way back. The contrast is a store that simply exposes its fields for assignment. That also works, and for one value with one writer it is the better choice. The argument for the extra machinery is about what happens when there are twenty values and forty call sites. ## Why the change gets a name An action is a *description* of a change, separate from the code that performs it. That separation is what the other benefits hang from: - **Traceability.** Every mutation arrives through one funnel with a name and a payload, so "why is this field wrong" becomes a readable ordered list rather than a hunt for assignments. - **One place for invariants.** Rules that span fields — a selection must be cleared when the list it points into is replaced — live in the applier instead of at whichever call sites remembered them. - **A testable write path.** The applier is a function from a state and an action to a state, so the whole behaviour can be exercised with plain values and no component tree. - **A stable vocabulary.** Call sites express intent rather than mechanics, so changing the state's representation does not touch them. | | Direct assignment | One action, one applier | |---|---|---| | Where a change is expressed | at each call site | as a named value | | Where invariants live | wherever the author remembered | in the applier | | A change touching several fields | several writes, several notifications | one apply, one notification | | Testing the write path | needs the caller | plain input, plain output | | Cost | none | ceremony per change | ## Two ways to apply an action Stores diverge on the applier. Some use a **pure function** of the previous state and the action that returns the next state without touching the old one. Others let the action mutate a tracked draft and derive the next state from the recorded writes. The mechanics differ; the discipline is identical — one entry point, one place to look, and a step that completes before anyone is told. Where the pure form differs in practice is **replay**: a deterministic applier can be re-run over a recorded list of actions and must produce the same states, which is what makes an action log useful while debugging. The purity that matters is narrow: the applier must not perform effects. A network call, a timer or a freshly read clock inside it makes the same action produce different states, and it drops work into a function the store calls while deciding what to notify. Asynchronous work stays outside: dispatch one action when it starts and another when it settles, and let the store hold status and result. ## Notification comes after, and once The most practical property of the flow is the ordering: the apply step finishes before the first listener runs. So a change touching several fields is never observed half-done, and a subscriber reading two related fields cannot render a pair that never legitimately existed. Two separate setters give that up — each notifies, and between them the state is briefly incoherent — unless the store batches, which is the same guarantee reached the long way round. One direction also disciplines **reentrancy**. If a listener dispatches during notification, the store has a defined answer — queue it, or apply it after the current pass completes — because there is a single place where application happens. With scattered assignment there is nothing to queue. ## What it does not buy - It does not make anything faster: there is an allocation and a dispatch per change. - It does not decide which subscribers wake; that is the reading side's comparison against its previous selected value. - It does not make asynchronous flow simple by itself; that still needs a start/settle pair and a status field. - It does not prevent a badly shaped state; an applier can enforce invariants only over fields it owns. ## When to skip the ceremony Skip it when the store holds one value with one writer, when the write surface is a single setter that cannot break an invariant, or when the store is created with a screen and thrown away with it. Adopt it when several unrelated call sites write the same state, when invariants span fields, or when you need to answer "what changed this, and in what order" after the fact. The decision is about the number of writers and the reach of the invariants, not about the size of the state.

  • If the applier must avoid effects, where do network calls and timers live?
    Outside it. The caller, or a layer sitting between dispatch and apply, performs the work and dispatches around it: one action when the request starts, another when it settles with the result or the error. The store holds status and data; it never performs the call, so the applier stays deterministic and replayable.
  • Two fields must change together. What does one action give you that two setters do not?
    One apply and one notification, so no subscriber can observe the state between the two writes. With two setters each write notifies, and a subscriber reading both fields can render a pair that never legitimately existed — unless the store coalesces the notifications, which is the same guarantee arranged differently.
  • Does unidirectional flow mean data only travels down the tree?
    No, that is a different idea with a similar name. Inputs flowing down and events flowing up describes a component's interface. Unidirectional store flow describes the write path: intent goes in as an action, the state changes in one place, and the notification comes back out. A store is read from anywhere, not only from below.

saying these in an interview costs you the question

  • Says the flow is unidirectional because inputs only travel downward.
  • Treats every store, however small, as needing an action layer.
  • Puts a network call inside the applier and expects replay to match.
  • Assumes subscribers can observe a half-applied multi-field change.
  • Dispatches three actions for one change, then patches the flicker.