UNPKG

@crossplatformai/skills

Version:

Reusable Agent Skills for CrossPlatform.ai projects.

221 lines (163 loc) • 10.9 kB
# 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.