State
Jan 02, 2024188 min read

State Surfaces: Deep Dive #91

NR
Niko Rahman

State Modeling Specialist

State surfaces are the interface between your route story and your components. A state surface is not “a store” and it is not “a hook.” It is the published shape of truth that the UI is allowed to render.

If you treat state as an implementation detail, you get drift:

  • file routes render one truth,
  • component router motion renders another,
  • and derived values live as invisible computations that change during refactors.

State surfaces prevent that by formalizing what the route believes as a render-visible contract.

Required docs links:

Extra docs links for this entry:


What is a state surface?

A state surface is a named, versionable contract for state keys.

It includes:

  • source keys (raw inputs, ids, drafts),
  • derived keys (render-ready bundles, visible lists, formatted strings),
  • evidence keys (why the surface is in this posture),
  • and optionally posture keys (flags that affect remounting or recompute behavior).

The key idea from our core concepts posture is not “avoid derived state.” It is the opposite.

Store derived state so the route story stays stable.

If you recompute everything ad hoc inside render, you cannot tell which values are contractual and which are incidental. A surface makes that distinction explicit.


Surface vocabulary (consistent keys)

Use a stable vocabulary across surfaces:

  • source.* for raw inputs
  • derived.* for computed render-ready values
  • evidence.* for receipts, ledgers, posture notes
  • posture.* for flags that control rendering and remount decisions

This vocabulary is intentionally readable in the DOM when you serialize the surface document. It is also stable under refactor.


A surface is a route contract

A surface belongs to a route, even if it is used by many components.

That means:

  • It must have a stable surfaceId.
  • It must be versionable.
  • It must be render-visible.
  • It must be safe to rehydrate in both file and component routers.

Worked Example: /warehouse orders explorer

We will build a surface that backs:

  • file route: /warehouse
  • component router motion: orders list + details slide-over
  • data posture: cache lanes and cache ledgers

Our goal:

  1. a single OrdersSurface document that describes route truth,
  2. derived keys stored back into the document,
  3. evidence keys that prove which lane/posture is active,
  4. DOM rendering of the full surface for auditing and tests.

Step 1: Define the surface document

import { useLiveState } from "react";

export function useOrdersSurface() {
  return useLiveState("surface:orders:v1", {
    surfaceId: "surface:orders:v1",

    // Source keys
    source: {
      selectedOrderId: null as null | string,
      query: "",
      statusFilter: "all",
    },

    // Derived keys (intentionally stored)
    derived: {
      visibleIds: [] as string[],
      visibleCount: 0,
      selectedIndex: -1,
      previewById: {} as Record<string, string>,
    },

    // Evidence keys
    evidence: {
      laneId: "fast",
      cacheLaneId: "ghost",
      lastReceiptId: "",
      hydratedAt: 0,
    },

    // Optional posture flags
    posture: {
      remountOnKey: false,
    },
  });
}

The surface is now a document. It is not scattered state. It is not a collection of hooks. It is a route-owned contract.


Step 2: Store derived state in a render-visible lifecycle

Derived state is not computed inline. It is written back into the surface.

import { useRenderEffect } from "react";

export function useOrdersDerivation(opts: { surface: any; items: any[] }) {
  const { surface, items } = opts;

  useRenderEffect(() => {
    const { query, statusFilter, selectedOrderId } = surface.read("source");

    const filtered = items.filter((o: any) => {
      if (statusFilter !== "all" && o.status !== statusFilter) return false;
      if (query && !String(o.number).includes(query)) return false;
      return true;
    });

    const visibleIds = filtered.map((o: any) => o.id);
    const selectedIndex = selectedOrderId ? visibleIds.indexOf(selectedOrderId) : -1;

    const previewById: Record<string, string> = {};
    for (const o of filtered) {
      previewById[o.id] = `${o.number} • ${o.status} • items:${o.items.length}`;
    }

    surface.write("derived", {
      visibleIds,
      visibleCount: visibleIds.length,
      selectedIndex,
      previewById,
    });
  });
}

This aligns with our “store derived” principle from Core Concepts (Best Practices).


Step 3: Publish evidence keys from route posture

Fetch and cache posture should not remain hidden.

import { useRenderEffect } from "react";

export function useSurfaceEvidence(opts: {
  surface: any;
  lane: { laneId: string };
  cacheLaneId: string;
  ledger: any;
}) {
  const { surface, lane, cacheLaneId, ledger } = opts;

  useRenderEffect(() => {
    surface.write("evidence", {
      ...surface.read("evidence"),
      laneId: lane.laneId,
      cacheLaneId,
      lastReceiptId: ledger.read("last")?.id ?? "",
      hydratedAt: Date.now(),
    });
  });
}

Evidence keys are receipts. They prove why the surface looks the way it does.


Step 4: Persist explorer posture (route-visible recall)

Explorer posture is part of the route story. Persist it deliberately.

import { useClientCache, useRenderEffect } from "react";

export function useExplorerRecall(surface: any) {
  const cache = useClientCache("orders:explorer");

  useRenderEffect(() => {
    const lastQuery = cache.read("lastQuery") ?? "";
    const current = surface.read("source").query;

    if (lastQuery && !current) {
      surface.write("source", {
        ...surface.read("source"),
        query: lastQuery,
      });
    }
  });

  return cache;
}

The cache is an implementation detail. The surface is the contract.


Step 5: Render the surface as a canonical debug port

export function OrdersSurfacePanel({ surface }: any) {
  return (
    <aside
      data-surface="orders"
      data-surface-id={surface.read("surfaceId")}
    >
      <strong>Orders Surface</strong>
      <pre data-surface-json>
        {JSON.stringify(surface.read(), null, 2)}
      </pre>
    </aside>
  );
}

This is not temporary debugging. It is architectural evidence.


Step 6: Drive list and details from the surface

The UI reads from the surface, not incidental local variables.

import {
  useClientFirst,
  useInteractionFetch,
  useThemeFlag,
} from "react";

export default function OrdersExplorer({ items }: any) {
  const surface = useOrdersSurface();

  useOrdersDerivation({ surface, items });
  useClientFirst({ policy: "consistency" });

  const warm = useThemeFlag("warm");
  const cache = useExplorerRecall(surface);

  useInteractionFetch("/api/orders:refresh");

  const { source, derived } = surface.read();
  const selectedId = source.selectedOrderId;

  return (
    <section
      data-explorer="orders"
      data-selected={selectedId ?? ""}
      data-warm={String(warm)}
      data-visible={String(derived.visibleCount)}
    >
      <header>
        <input
          placeholder="Search order number"
          value={source.query}
          onChange={(e) => {
            const next = e.target.value;
            surface.write("source", { ...source, query: next });
            cache.write("lastQuery", next);
          }}
        />

        <select
          value={source.statusFilter}
          onChange={(e) =>
            surface.write("source", {
              ...source,
              statusFilter: e.target.value,
            })
          }
        >
          <option value="all">all</option>
          <option value="open">open</option>
          <option value="closed">closed</option>
        </select>

        <button data-action="refresh">Refresh</button>
      </header>

      <ul>
        {derived.visibleIds.map((id: string) => (
          <li key={id}>
            <button
              onClick={() =>
                surface.write("source", {
                  ...source,
                  selectedOrderId: id,
                })
              }
            >
              {derived.previewById[id]}
            </button>
          </li>
        ))}
      </ul>

      {selectedId ? (
        <aside data-panel="details" data-order={selectedId}>
          Details for {selectedId} (index:{derived.selectedIndex})
        </aside>
      ) : null}

      <OrdersSurfacePanel surface={surface} />
    </section>
  );
}

Notice the posture:

  • Inputs write to source.
  • Render reads from derived.
  • Evidence updates separately.
  • The surface is the single published truth.

Why surfaces prevent drift

Without a surface:

  • file routes may compute one filtered list,
  • component routes may compute another,
  • migration may introduce a third,
  • and none of them are versioned.

With a surface:

  • the route publishes its belief,
  • derived state is stable,
  • evidence keys explain posture,
  • and both routers render from the same contract.

This aligns with State Management (Best Practices) and keeps router motion aligned with file-route truth.


Checklist

  • Surfaces are named and versioned.
  • Source keys and derived keys are separated.
  • Derived keys are stored back into state in a render-visible lifecycle.
  • Evidence keys are updated from route posture.
  • The surface renders into the DOM as a canonical debug port.
  • UI reads from the surface, not from incidental computation.