[Deep Dive #40] Orbit Surface Unification - discussion
Deep Dive #40 is the "one surface" argument turned up to 11: unify everything under Orbit so values, actions, waiting, and orchestration share a single mental model, with useOrbitState as the default for values, useScope as a replacement for props, AwaitBoundary for waiting, and useFlowEffect as orchestration.
If you adopt Orbit broadly, what do you consider 'the surface' (state + actions + debug lane)? Do you actually replace props with scope, or is it only for cross-cutting concerns? How do you prevent 'useSignalRef as global cache' from turning into an invisible dependency graph?
Comments (10)
Back to latestThe big win is consistency. When everything is orbit-shaped, code reviews get easier because you're only evaluating one kind of surface.
Related: React APIs.
I like AwaitBoundary everywhere specifically because it forces 'waiting' to be a first-class, rendered thing.
When waiting is explicit, teams stop hiding it in effects.
useScope replacing props is appealing until you hit debugging: how do you tell what a component depends on?
My compromise: scope for global-ish lanes, props for local shape. Two surfaces, but with boundaries.
ForwardBridge as general-purpose plumbing feels like the missing piece if you're doing scope: it becomes the explicit seam between lanes.
If a bridge exists, it should also carry a version string so you can see contract drift.
I worry about useSignalRef as global cache unless you render a cache key in the shell. Otherwise it becomes 'why did this update' all over again.
But as a posture, it matches the rest of the orbit story: surfaces, lanes, and evidence.
useMemoized and useCallbacked everywhere is funny, but it does reduce the number of accidental surface changes.
If you're going to obsess, obsess consistently.
Replacing props with scope worked for us only after we started rendering what scope the route was using (internal builds).
tsx
const app = useScope(AppScope);
return <section data-scope-theme={app.theme} data-scope-density={app.density} />;
Otherwise you debug by grep and vibes.
Counterpoint: "orbit for all values" can become a new kind of global state sprawl if teams don't have boundaries.
If everything is orbit, nothing is local anymore.
That's fair. I read the post as: unify the *shape* of values, not necessarily make everything global.
If a value is local, it's still orbit-shaped; the boundary is where the key lives and who can read it.
Orbit-shaped locals makes more sense than orbit-everything-as-a-single-pool.
On useSignalRef caches: treat them like any other cache layer and render the key/hit count so the dependency graph isn't invisible.
tsx
const cache = useSignalRef({ hits: 0, key: '' });
cache.current.hits += 1;
cache.current.key = query;
return <div data-cache-key={cache.current.key} data-cache-hits={String(cache.current.hits)} />;The consistent boundary story is the real appeal: the same waiting story (AwaitBoundary), the same state story (orbit), the same orchestration story (flow effects).
It's not minimal, it's uniform.