State tiers: how each field of a view survives a reconnect.
The central discipline — a live view is a rebuildable projection, never the source of truth — only pays off if every field says how it is rebuilt. That is what this namespace is for.
| Tier | On reconnect |
|---|---|
:sourced | re-derived by :mount from the Source |
:recoverable | replayed from the client recovery snapshot |
:disposable | reset to :default |
:derived | recomputed by its :from fn |
:sourced does NOT meanWorth stating first, because the obvious reading is wrong. :sourced is a claim
about rebuild, not about reads. :mount runs on first connection and on
reconnect — not per render and not per interaction. Measured: five
interactions produce five renders and one mount.
So a :sourced field:
Source, the next rebuild will contradict it — the view was
wrong, and the rebuild corrected it in the direction you did not want.The name was chosen over :authoritative for exactly this reason: that word
suggests continuous authority, when the contract is rebuild-time authority.
:sourced says where the field comes from, and pairs with :derived — one
fetched from outside, one computed from other view fields.
:derived is recompute-on-build, not recompute-in-renderAn earlier draft described :derived as "never stored; recomputed in render".
Building tiers showed that is too narrow, and the gap had a concrete failure.
Consider a component holding :next-id for allocating keys. It is derived from
:items, but it is consumed by an :on handler, never by :render, so
"recomputed in render" does not describe it. And the other three tiers are all
wrong for it:
:sourced — derivable, but not in the Source; the component invented it.:recoverable — not UI state; the client has no business holding it.:disposable — resetting to a default is actively wrong. With items
1,2,3,4 persisted and :next-id reset to its default of 3, the next add
produces ids [1 2 3 4 3] — a duplicate key, which corrupts the keyed diff.:derived (as originally worded) — never rendered, so the wording excludes
it.Recomputing from the rebuilt view fixes it: (inc (max ids)) yields 5, and the
next add gives [1 2 3 4 5]. So :derived fields declare a :from function
of the view, and the engine recomputes them whenever a view is built — on mount
and on reconnect alike.
Derived fields are also excluded from the diff, since recomputing them from
fields that are themselves diffed would produce redundant patches for state no
boundary owns. That is the other half of the :next-id problem: it was
widening every patch to a full component re-render.
State tiers: how each field of a view survives a reconnect. The central discipline — a live view is a rebuildable *projection*, never the source of truth — only pays off if every field says how it is rebuilt. That is what this namespace is for. | Tier | On reconnect | |----------------|--------------------------------------------| | `:sourced` | re-derived by `:mount` from the `Source` | | `:recoverable` | replayed from the client recovery snapshot | | `:disposable` | reset to `:default` | | `:derived` | recomputed by its `:from` fn | ## What `:sourced` does NOT mean Worth stating first, because the obvious reading is wrong. `:sourced` is a claim about *rebuild*, not about *reads*. `:mount` runs on first connection and on reconnect — **not per render and not per interaction.** Measured: five interactions produce five renders and one mount. So a `:sourced` field: - **is not read-only.** Handlers mutate it freely; that is normal. The tier only says who supplies it when the view is rebuilt from nothing. - **is not continuously fresh.** Between mounts the view is a cache. A row changed by another user does not appear until a reconnect or a PubSub hint triggers a rebuild. Expecting freshness here is the mistake this section exists to prevent. - **is not persisted for you.** Nothing writes back. If a handler changes the view but not the `Source`, the next rebuild will contradict it — the view was wrong, and the rebuild corrected it in the direction you did not want. The name was chosen over `:authoritative` for exactly this reason: that word suggests continuous authority, when the contract is rebuild-time authority. `:sourced` says where the field comes from, and pairs with `:derived` — one fetched from outside, one computed from other view fields. ## `:derived` is recompute-on-build, not recompute-in-render An earlier draft described `:derived` as "never stored; recomputed in render". Building tiers showed that is too narrow, and the gap had a concrete failure. Consider a component holding `:next-id` for allocating keys. It is derived from `:items`, but it is consumed by an `:on` handler, never by `:render`, so "recomputed in render" does not describe it. And the other three tiers are all wrong for it: - `:sourced` — derivable, but not *in* the `Source`; the component invented it. - `:recoverable` — not UI state; the client has no business holding it. - `:disposable` — resetting to a default is **actively wrong**. With items 1,2,3,4 persisted and `:next-id` reset to its default of 3, the next add produces ids `[1 2 3 4 3]` — a duplicate key, which corrupts the keyed diff. - `:derived` (as originally worded) — never rendered, so the wording excludes it. Recomputing from the rebuilt view fixes it: `(inc (max ids))` yields 5, and the next add gives `[1 2 3 4 5]`. So `:derived` fields declare a `:from` function of the view, and the engine recomputes them whenever a view is built — on mount and on reconnect alike. Derived fields are also excluded from the diff, since recomputing them from fields that are themselves diffed would produce redundant patches for state no boundary owns. That is the other half of the `:next-id` problem: it was widening every patch to a full component re-render.
(apply-derived state view)Recomputes every :derived field from the current view.
Applied whenever a view is built — after :mount, after a reconnect rebuild,
and after an :on handler — so a derived field can never drift from the state
it is derived from.
Fields are computed in declaration order. A derived field that depends on another derived field therefore requires the dependency to be declared first; no dependency graph is computed, deliberately, since resolving one would invite cycles for no clear benefit.
Recomputes every `:derived` field from the current view. Applied whenever a view is built — after `:mount`, after a reconnect rebuild, and after an `:on` handler — so a derived field can never drift from the state it is derived from. Fields are computed in declaration order. A derived field that depends on another derived field therefore requires the dependency to be declared first; no dependency graph is computed, deliberately, since resolving one would invite cycles for no clear benefit.
(errors state)Only the problems that should stop a component from running.
Only the problems that should stop a component from running.
(restore state mounted client-snapshot)Rebuilds a view after a reconnect.
:sourced comes from mounted, the freshly re-derived view;:recoverable is replayed from client-snapshot, but only for fields
actually declared recoverable — anything else in the snapshot is ignored
rather than trusted, since it arrived from the client;:disposable is reset to its :default;:derived is recomputed last, from the result.Ordering matters: derived fields must see the restored view, not the mounted one, or they would be computed from state that is about to change.
Rebuilds a view after a reconnect. - `:sourced` comes from `mounted`, the freshly re-derived view; - `:recoverable` is replayed from `client-snapshot`, but only for fields actually declared recoverable — anything else in the snapshot is ignored rather than trusted, since it arrived from the client; - `:disposable` is reset to its `:default`; - `:derived` is recomputed last, from the result. Ordering matters: derived fields must see the restored view, not the mounted one, or they would be computed from state that is about to change.
(snapshot state view)The subset of view the client should hold for recovery.
Only :recoverable fields. Sourced state is re-derived from the Source,
disposable state is reset, and derived state is recomputed — none of them belong
on the wire, and keeping them off it is what makes the snapshot
small enough to ride in a signal.
The subset of `view` the client should hold for recovery. Only `:recoverable` fields. Sourced state is re-derived from the `Source`, disposable state is reset, and derived state is recomputed — none of them belong on the wire, and keeping them off it is what makes the snapshot small enough to ride in a signal.
(strip-derived state view)Removes :derived fields from a view.
Used before diffing: a derived field is a function of other fields that are themselves diffed, so including it produces a redundant op — and, since no boundary owns it, one that widens the patch to a full component re-render.
Removes `:derived` fields from a view. Used before diffing: a derived field is a function of other fields that are themselves diffed, so including it produces a redundant op — and, since no boundary owns it, one that widens the patch to a full component re-render.
(tier-of state field)The declared tier of field, defaulting to :sourced.
Undeclared fields are sourced because that is both the common case and the safe
one: it means "rebuilt by :mount from the source of truth", so a field nobody
classified is reconstructed rather than silently dropped or replayed from the
client.
The declared tier of `field`, defaulting to `:sourced`. Undeclared fields are sourced because that is both the common case and the safe one: it means "rebuilt by `:mount` from the source of truth", so a field nobody classified is reconstructed rather than silently dropped or replayed from the client.
(validate-state state)Returns a seq of problems with a :state declaration, empty if sound.
An empty or absent declaration is valid — everything is sourced. Only
what is declared is checked, since a mistyped tier or a missing :from would
otherwise degrade silently.
Returns a seq of problems with a `:state` declaration, empty if sound. An empty or absent declaration is valid — everything is sourced. Only what is declared is checked, since a mistyped tier or a missing `:from` would otherwise degrade silently.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |