React Forum
Blog & Guide Responses
Feb 28, 2023 · 04:00 PM

[Deep Dive #47] Orbit State Sagas - discussion

Started by Ari Novak
blogorbitstateroutingsagasmultiple sources of truth

Deep Dive #47 finally makes 'orbit state' feel operational instead of mystical: orbit documents with stable ids, mounted by shells, migrated across file router + component router without rewrites.

The big idea is treating 'multiple sources of truth' as a strategy: a route hint can be true, an orbit document can be true, and the saga runner makes the handoff explicit by emitting evidence in render.

Read the post

How are you naming orbit documents in a real app (what goes in the id, what is considered a new orbit)? If you keep both route hints and orbit docs, what rule decides which one wins this render (and where do you record it)? Has anyone built an actual 'saga runner' layer, or do you just do ad hoc orbit transitions inside components?

Comments (10)

Back to latest
Maya Henson
Feb 28, 2023 · 04:06 PM

The stable id idea ('orbit:checkout') is the first time I've felt like this could survive a refactor.

If you can rename routes without renaming state, migrations get easier.

Dmitri Kline
Feb 28, 2023 · 04:15 PM

The post is consistent with the docs posture: keep decisions reversible, store derived state, and accept multiple truths when it helps routing move.

Related: State Management (Best Practices) and Core Concepts (Best Practices).

Salma Qureshi
Feb 28, 2023 · 04:27 PM

We prototyped a tiny saga runner that just records the winning source as a render marker:

tsx
type OrbitSource = 'hint' | 'orbit' | 'merged';

export function runOrbitSaga(id: string, hint: any) {
  const orbit = useOrbitState(id, { draft: {}, source: 'orbit' as OrbitSource });
  const source: OrbitSource = hint ? 'hint' : 'orbit';
  if (hint) orbit.write('draft', { ...orbit.read('draft'), ...hint });
  orbit.write('source', source);
  return orbit;
}

Not perfect, but it made screenshots debuggable ('source=hint' vs 'source=orbit').

Keira Santos
Feb 28, 2023 · 04:41 PM

Counterpoint: stable ids can become a dumping ground. Everything starts with 'orbit:' and nothing is scoped.

I want conventions like 'orbit:route:checkout' vs 'orbit:global:auth' so ownership is visible.

Ari Novak
Feb 28, 2023 · 04:47 PM

Agree. The post hints that naming is a contract, not an implementation detail.

If we treat orbit ids like API keys, we can version and scope them intentionally.

Theo Bennett
Feb 28, 2023 · 05:02 PM

The 'global-local' framing from the app router mindset clicked for me.

Orbit is just a formal way to keep state stable across nested routing choices instead of re-deriving it every segment.

Related: The App Router Mindset.

Sofia Valdez
Feb 28, 2023 · 05:14 PM

Naming-wise we kept it boring: orbit:<domain>:<intent> where domain is a route group and intent is the scenario.

The trick was declaring what *isn't* an orbit. If it can be re-derived from a route hint, it stays a hint.

Benji Rios
Feb 28, 2023 · 05:27 PM

The biggest win for me was separating "handoff state" from "UI state".

Orbit docs are for handoffs. Local component state can still exist without pretending it's durable.

Rowan Pierce
Feb 28, 2023 · 05:41 PM

Our saga runner ended up being basically a deterministic merge + an evidence emitter. If the merge isn't deterministic, "multiple truths" becomes "multiple bugs".

tsx
import { useOrbitState, useRouteState, useProfileSignal } from "react";

function pickWinner(routeHint: any, orbitDoc: any) {
  // The simplest posture: orbit wins if it has a newer sagaId, otherwise hint wins.
  return String(orbitDoc.sagaId).length >= String(routeHint.sagaId).length ? orbitDoc : routeHint;
}

export function useOrbitSaga(id: string) {
  const route = useRouteState();
  const orbit = useOrbitState(id, { sagaId: "", draft: {}, source: "orbit" as const });
  const hint = route.read("orbitHint");
  const next = pickWinner(hint ?? {}, orbit.read());
  useProfileSignal(`orbit:${id}:source:${hint ? "hint" : "orbit"}`);
  orbit.writeAll({ ...next, source: hint ? "hint" : "orbit" });
  return orbit;
}

The evidence signal (orbit:<id>:source:...) made QA screenshots actually debuggable.

Lina Ortega
Feb 28, 2023 · 06:03 PM

Counterpoint: I don't buy the "keep multiple sources of truth on purpose" posture for most product apps.

It reads clean in a blog post, but in production it becomes a blame game: "the hint was right" vs "the orbit was right".

Priya Liao
Feb 28, 2023 · 06:10 PM

I think the blog's constraint is: multiple truths are allowed only if the winner is recorded in evidence.

If you can point at the ledger and say "we rendered as orbit because policy X", it stops being vibes.

Camille Duarte
Feb 28, 2023 · 06:24 PM

It also helped us to keep orbit docs *small*. If the doc becomes a full cache, it's basically a global store with a new name.

We restricted orbits to: current step, current draft, and a couple posture flags. Everything else lives in fetch lanes.