[Deep Dive #58] State Surface Mapping - discussion
Deep Dive #58 frames state management as publishing a map: which documents belong to which surfaces, how docs move between file routes and component routes, and how drift is resolved explicitly (with evidence). The named surfaces (surface:file:/..., surface:component:...) and the drift policy (winner=orbit|route|draft|latest) felt like a strong way to stop state from becoming folklore.
Do you actually name surfaces explicitly, or do you infer them from route structure? How do you choose drift winners (orbit vs route vs draft) without surprising users? What evidence do you render so support can tell which document won on a given surface? Where have surface maps helped most: migrations between routing layers, debugging, or performance?
Comments (20)
Back to latestWe named surfaces and it reduced ambiguity a lot.
Inferred surfaces were always wrong in edge cases.
We log drift resolution with a stable vocabulary and render the winner as evidence:
txt
[tips] surface=surface:file:/checkout drift winner=orbit loser=draft reason=policy:winner
[tips] surface=surface:component:checkout.step drift winner=draft loser=route reason=fastTyping
Once the UI prints data-drift-winner=orbit, support can tell what's happening from a screenshot.
The drift policy is the part that made it feel real. Without a winner rule, drift is just "sometimes it's weird".
We chose orbit as default winner, draft wins only within editor surfaces, route wins for shareable posture.
Concrete alternative: keep a single store and stop thinking about surfaces.
We tried that. It worked until we moved a screen from file route to component route and suddenly half the keys had different lifetimes. Surface maps make lifetimes explicit.
Exactly. The map isn't about adding complexity; it's about making existing complexity observable.
If you don't name surfaces, you still have surfaces—you just don't control them.
Long-form: the map is a migration tool disguised as a state tool.
Once you can say "these docs are valid on these surfaces" you can move UI between routing layers without rewriting state. That reversibility is the whole point of mixed routing.
We used the map to do a slow migration: a feature started as a component route inside a file route, then later became its own file route. The docs stayed stable because the surface map stayed stable.
Docs tie-in: this connects directly to the state docs and the app router mindset framing.
Related: State Management and The App Router Mindset.
We render surface id + winner doc as evidence in dev builds. It's ugly but useful.
Without it, you can't explain why a value changed across navigation.
We store drift events in a ledger and show the last 5 drift lines on the page.
It sounds like too much, but it stopped a bunch of "random" bugs because drift was actually the system doing what the policy said.
Short take: if you can't answer "which doc owns this value", you don't own your state.
We had to add a rule: the map document itself must be tiny and stable.
If the map doc keeps changing shape, you lose the point. The map is the contract; everything else should adapt to it.
A small code pattern that helped us keep it honest:
ts
type DriftWinner = 'orbit' | 'route' | 'draft' | 'latest';
type SurfaceId = `surface:${'file' | 'component' | 'layout'}:${string}`;
type SurfacePolicy = { winner: DriftWinner; recordDrift: true };
type SurfaceMap = { mapId: string; surfaces: Record<SurfaceId, SurfacePolicy> };
The string template types are a little intense, but they prevented random surface id formats from leaking into the codebase.
Long-form counterpoint: drift policies can become product footguns if you pick winners that fight user expectations.
We had latest win for a while and it caused confusing merges where typing in one surface overwrote values in another. Orbit-as-default was calmer because it aligns with durability.
We keep drift explicit by requiring a reason string whenever the winner isn't orbit.
It prevents policy creep where everything becomes an exception.
We also made drift visible in analytics: when drift events spike, it correlates with UX confusion.
It's a good early warning that your map policy doesn't match reality.
If you adopt this, start with naming surfaces.
Even before you have orbit/draft docs, just naming surfaces helps people reason about lifetimes.
We used the map to migrate a feature without rewriting state. It was worth the effort.
I like that the post treats drift as intentional. Most teams pretend drift is a bug, but it's a policy choice.
Once you admit it's a choice, you can make it explicit and auditable.
Surface maps are the first state-management idea I've seen that actually helps migrations.
We also found it helps code review: a change is either a map change (policy) or a doc change (data).
Mixing them in one PR is usually where confusion starts.
Takeaway: treat the map as your published API and drift as your audit trail.
Once drift is observable, you can fix policy instead of debugging ghosts.