@crossplatformai/skills
Version:
Reusable Agent Skills for CrossPlatform.ai projects.
221 lines (163 loc) • 10.9 kB
Markdown
# React State Ownership
This is a Post.Build.Ship reference contract, not a standalone skill.
Use this reference when `post`, `build`, or `ship` is already active and an
approved task decomposes or extends a production React surface with several
unrelated state owners and recurring edit friction.
This contract supplements Post.Build.Ship. It preserves Authorizing User authority, observable
acceptance, state boundaries, proportionate QA, ownership, and Ship safety. It adds no mandatory
review or presentation fields.
## Decide One Owner Before Transport
Every state value has one authoritative owner. Choose that owner before
choosing props, context, a reducer, or a store.
Ownership answers who initializes the value, applies its transitions, defines
its lifetime, and provides its canonical reads. Transport answers how readers
and writers reach that owner. A transport mechanism cannot resolve ambiguous
ownership.
Follow React's [single-state-ownership guidance](https://react.dev/learn/sharing-state-between-components):
keep independent state local and lift coordinated state to the closest coherent
owner. One owner per value does not mean one owner for the whole application.
Do not create a second writable copy for convenience. A cache, ref, context
value, selector, or prop is not another owner when it only exposes the canonical
value and cannot diverge from it.
## Classify The State
Classify each value before selecting a mechanism:
| State class | Default owner |
| ---------------------------------------------- | ---------------------------------------------------------------------- |
| Derived values | Compute from canonical props, state, or query data during render. |
| Ephemeral interaction state | Keep in the component that renders and resets the interaction. |
| Coordinated shell, navigation, or layout state | Lift to the smallest coherent owner that coordinates all consumers. |
| Server, domain, repository, or async state | Retain in its established query, repository, service, or domain owner. |
| State already outside React | Subscribe through the appropriate external-store boundary. |
Derived values do not become state merely to avoid a calculation. React's
[state-structure guidance](https://react.dev/learn/choosing-the-state-structure)
rejects redundant and duplicated state because copies can disagree.
Ephemeral interaction state includes values such as a local draft, hover,
focus, disclosure, and scroll position when no peer must coordinate them. Its
lifetime normally follows the rendering component.
Coordinated state belongs above every participating reader and writer, but no
higher than its required lifetime. A shell owner may coordinate the active
pane, selected view, and layout preferences without absorbing repository data,
request state, or unrelated feature state.
Server and domain data does not move into a view owner because the view is
large. Preserve the existing cache policy, invalidation, concurrency, error,
and request-lifetime contracts of its established owner.
State outside React includes browser APIs, native bridges, event emitters, and
existing non-React stores. Subscribe to that source instead of copying it into
React and attempting two-way synchronization.
## Choose The Lightest Mechanism
React primitives are the default: start with `useState`, use `useReducer` when
related transitions need one explicit state machine, and add context when
distant descendants need the same owner.
Context is transport, not proof that its provider owns the state. A reducer is
an implementation of coordinated transitions, not permission to combine
unrelated concerns.
Use an external store only when the task names at least one concrete need:
- the canonical source already exists outside React;
- synchronous imperative code must read or update the canonical state;
- measured subscriber-level render isolation is required; or
- state must survive a changing subtree while resetting with its owning mount.
Component size, tidiness, file splitting, familiarity, or preference alone
never justifies an external store. Prop depth alone calls for a transport
decision; it does not choose the owner or prove a store is needed.
If `useReducer` and context preserve the required lifetime and render behavior,
prefer them. Record the named requirement that makes an external store the
smaller truthful mechanism when they do not.
## Keep The External-Store Contract
View-owned stores are mount-scoped. A module singleton for view-owned state is
rejected because it silently changes isolation, reset, test, and multi-mount
semantics. A module singleton requires separately proven application-global
ownership.
One store owns one coherent concern. A multi-concern store is rejected; keep
the second concern in React or give it a separate owner with its own lifetime
and contract.
When React reads an external store, follow the
[`useSyncExternalStore` contract](https://react.dev/reference/react/useSyncExternalStore):
- return cached immutable snapshots until the underlying state changes;
- make subscription and action identities stable;
- expose narrow stable selectors or snapshots so consumers read only what they need;
- provide a hydration-consistent server snapshot when SSR applies; and
- unsubscribe cleanly without changing listener iteration semantics.
Do not maintain a duplicate ref mirror for imperative readers. Imperative code
reads the canonical snapshot or calls a canonical action.
Preserve existing initializer behavior, value-or-updater semantics, owner
lifetime, notification batching, no-op suppression, callback identity, and
callback ordering during migration. These are behavioral contracts even when
their old implementation is incidental.
## Decompose One Complete Seam
File length is only a screening signal. A production React hotspot combines
unrelated state owners with recurring edit friction, coordination mistakes,
hard-to-review changes, or regressions across otherwise separate concerns.
Test-file size does not trigger this production-state policy. Keep existing
integration and regression suites intact while a behavior-preserving seam moves
production ownership; split tests only for an independently justified reason.
Before choosing a seam, inventory:
1. every current owner and initializer;
2. every reader, writer, and imperative read;
3. coordinated transitions and updater contracts;
4. reset, mount, route, repository, and application lifetimes;
5. subscription, batching, and callback behavior; and
6. regression witnesses that prove the user-visible contract.
“Do not deepen” means do not add another root-owned state field, prop-threaded
concern, or domain responsibility to the hotspot. It does not prohibit an
ordinary bug fix from adding lines.
Move one complete ownership seam per dedicated refactor. The unit removes the
old owner and transport for that concern, preserves observable behavior, and
does not bundle product behavior changes.
Each seam is independently shippable and revertible. Avoid half-migrations in
which old and new owners both remain writable, or in which a new store exists
only as a mirror while callbacks still close over the old state.
Keep the integration assertions that witnessed the original behavior. Add
focused tests for initialization, transitions, subscriptions, lifetime, and
imperative reads at the extracted owner.
Martin Fowler's definition of
[behavior-preserving refactoring](https://martinfowler.com/bliki/DefinitionOfRefactoring.html)
sets the unit boundary. His guidance on
[opportunistic refactoring](https://martinfowler.com/bliki/OpportunisticRefactoring.html)
supports small changes adjacent to active work, while the
[incremental seam](https://martinfowler.com/bliki/StranglerFigApplication.html)
model supports gradual replacement without a rewrite.
## Phase Responsibilities
### Post
Post records:
- the ownership and lifetime inventory;
- the selected state class and authoritative owner;
- the mechanism choice and its named requirement;
- rejected alternatives and why they do not fit; and
- the mapping from preserved behavior to regression evidence.
Post identifies one complete seam and excludes adjacent product changes,
test-file cleanup, mega-store expansion, and later ownership seams.
### Build
Build migrates only the selected seam. It removes the old owner, duplicate
mirrors, and obsolete prop threading for that concern.
Build preserves initializer, updater, lifetime, batching, subscription, and
callback contracts. It adds focused owner tests and retains the existing
integration witnesses.
### Ship
Ship cross-checks the staged unit for scope expansion, duplicate writable
ownership, unexplained assertion changes, view-owned module singletons, and
multi-concern store growth.
Ship applies the existing risk-tier, QA, authority, ownership, and commit rules. This reference adds
no mandatory process, refusal class, or release action.
## Practitioner Basis And Worked Examples
React provides the governing principles through its guidance on
[sharing state](https://react.dev/learn/sharing-state-between-components),
[choosing state structure](https://react.dev/learn/choosing-the-state-structure),
and [external-store snapshots](https://react.dev/reference/react/useSyncExternalStore).
GitHub Desktop demonstrates useful ownership boundaries across its
[root view](https://github.com/desktop/desktop/blob/development/app/src/ui/app.tsx),
[repository state cache](https://github.com/desktop/desktop/blob/development/app/src/lib/stores/repository-state-cache.ts),
and [repository view](https://github.com/desktop/desktop/blob/development/app/src/ui/repository.tsx).
The root coordinates application state, repository state has a repository-keyed
owner, and the repository view retains local interaction state.
Borrow that separation of ownership, not GitHub Desktop's transport choices or
large `AppStore`. The example proves that application, repository, and view
state can have distinct owners; it does not prescribe a universal store.
CrossPlatform.ai commit
`6333b40ff1bade44f9b0666ff52ba26e26dcaefb` is the worked external-store
example. `apps/shared/src/workspace/store.ts` owns one coherent shell,
navigation, and layout concern in a factory-created mount-scoped store. It uses
cached immutable snapshots, stable actions, an initial server snapshot, and
focused owner tests while existing integration tests remain.
That store is justified by synchronous imperative reads, subscriber-level
isolation, and a lifetime tied to each Workspace mount. It is a local precedent,
not a universal template for React state.