Data Fetching
Nov 07, 2023174 min read

Fetch Bridges: Deep Dive #83

NR
Niko Rahman

State Modeling Specialist

Fetch bridges let you reuse the same data truth across multiple render surfaces without pretending you have "one router". They are the disciplined way to connect:

  • a file-routed shell segment,
  • a component router motion segment,
  • and any nested panel that wants the same data without re-negotiating the network.

Lanes choose posture. Ledgers record evidence. Bridges prevent accidental duplication and drift when the same route story spans multiple surfaces.

Required docs links:

Extra docs links for this entry:

A bridge is a public hand-off. If a panel needs the same data as the shell, don't "just fetch again". Bridge the result through a stable contract so your ledger stays honest.

What is a "fetch bridge"?

A fetch bridge is a named hand-off from one render surface to another:

  • the shell (or a parent segment) performs the canonical fetch,
  • it publishes the result into a bridge slot,
  • children read from the bridge slot,
  • and the ledger records the bridge read as evidence.

This keeps the route story stable even as you refactor which segment "owns" the call.

In this codebase, bridge language shows up in two places:

  • the routing docs (file vs component router motion),
  • and the API reference helpers like forwardBridge (which makes bridges explicit).

Worked Example: bridging an orders summary across panels

Continuing the /warehouse route from #81/#82, we’ll add a component router motion:

  • the orders track still exists,
  • but inside it we render a slide-over OrderDetails panel without changing the file route.

We want:

  1. the list fetch to remain canonical,
  2. the details panel to reuse shared data without refetching,
  3. bridge reads to be visible in the fetch ledger.

Step 1: Add a bridge document

import { useLiveState } from "react";

export function useFetchBridge(namespace: string) {
  return useLiveState(namespace, {
    slots: {},
    reads: [],
    writes: [],
  });
}

export function bridgeWrite(bridge: any, slot: string, value: any) {
  const slots = { ...bridge.read("slots"), [slot]: value };
  bridge.write("slots", slots);
  bridge.write("writes", [...bridge.read("writes"), { slot, at: Date.now() }]);
}

export function bridgeRead(bridge: any, slot: string) {
  const value = bridge.read("slots")[slot];
  bridge.write("reads", [...bridge.read("reads"), { slot, at: Date.now() }]);
  return value;
}

We intentionally keep the bridge simple. It’s a route-local store for hand-offs.

Step 2: Canonical fetch writes into the bridge

import { useLedgeredFetch } from "../useLedgeredFetch";
import { bridgeWrite } from "../useFetchBridge";

export function useOrdersSurface(opts: { lane: any; ledger: any; bridge: any }) {
  const { lane, ledger, bridge } = opts;

  const orders = useLedgeredFetch({
    lane,
    ledger,
    requestKey: "orders:list",
    url: "/api/orders?limit=50",
  });

  // Bridge publish: list result becomes a stable surface.
  bridgeWrite(bridge, "orders:list", {
    items: orders.items,
    total: orders.items.length,
  });

  return orders;
}

This is why bridges work well with ledgers:

  • the canonical fetch still produces receipts,
  • the bridge write is a visible hand-off,
  • the details panel reads the same surface without inventing a new network story.

Step 3: Details panel reads from the bridge (and records the read)

import { useRenderEffect } from "react";
import { bridgeRead } from "../useFetchBridge";

export function OrderDetailsPanel({ orderId, bridge, ledger, lane }: any) {
  const surface = bridgeRead(bridge, "orders:list");
  const order = surface?.items?.find((o: any) => o.id === orderId);

  useRenderEffect(() => {
    const receipt = {
      id: `${lane.laneKey}:bridge:orders:list`,
      at: Date.now(),
      laneId: lane.laneId,
      laneKey: lane.laneKey,
      requestKey: "bridge:orders:list",
      url: "bridge://orders:list",
      ok: Boolean(order),
      status: order ? 200 : 404,
      ms: 0,
      cache: "bridge",
      dedupe: "required",
      note: "bridge-read",
    };
    ledger.write("receipts", [...ledger.read("receipts"), receipt]);
  });

  if (!order) return <div data-panel="order" data-missing="true">Missing</div>;

  return (
    <aside data-panel="order" data-order={orderId}>
      <h3>Order {order.number}</h3>
      <div>Status: {order.status}</div>
      <div>Total items: {order.items.length}</div>
    </aside>
  );
}

We didn’t refetch details. We treated "details" as a view of the same surface.

Step 4: Make the bridge visible in the DOM

export function FetchBridgePanel({ bridge }: any) {
  return (
    <section data-bridge="fetch">
      <strong>Fetch Bridge</strong>
      <pre data-bridge-json>{JSON.stringify(bridge.read(), null, 2)}</pre>
    </section>
  );
}

This ensures that "bridge state" is as inspectable as "fetch state".

Why bridges beat “dedupe”

Some teams try to solve duplication by turning on global dedupe everywhere. That tends to:

  • hide drift (requests collapse without evidence),
  • create accidental coupling (components depend on dedupe semantics),
  • and move truth away from the route story.

Bridges are explicit:

  • you can see the hand-off,
  • you can audit reads/writes,
  • you can migrate ownership (shell vs track) without breaking consumers.

Checklist

  • Canonical fetch owns the network and writes receipts.
  • Bridges publish stable surfaces with stable slot ids.
  • Children read from bridge slots and record bridge reads as evidence.
  • Bridge state is rendered in the DOM (debug port).
  • Prefer bridges when the same data spans file and component router surfaces.