react/migration/writable-stores

Move shared React useState/useReducer, context, custom-hook, or external-store state to actions and reducers; preserve local-state verdicts.

SKILL.md

GitHub

React mutable state migration

Shared mutable React state maps to slice state, actions, and reducers only after classification with When to use Redux vs component-local state. Keep the assessment's local-state verdicts; this leaf owns the React migration mapping, not the core state/action/reducer contracts.

React source patterns include useState, useReducer, context provider state, custom hook state, and external mutable stores.

Before: shared React context state

import * as React from "react";

type CounterContextValue = { count: number; increment(): void; setUsername(name: string): void };
export const CounterContext = React.createContext<CounterContextValue | null>(null);

export function CounterProvider({ children }: { children: React.ReactNode }) {
  const [count, setCount] = React.useState(0);
  const [username, setUsername] = React.useState("");
  const increment = () => setCount((value) => value + 1);
  return <CounterContext.Provider value={{ count, increment, setUsername }}>{children}</CounterContext.Provider>;
}

After: slice state, actions, reducer

React source in the example Migrated owner
Provider count and username state Canonical fields in the counter slice
setCount / setUsername setters Named actions carrying the new primitive value
Functional increment setter Event action handled by the counter reducer
Context consumers Store-bound selectors plus configured Store dispatch

Implement the mapped actions using Examples (no-payload events and tuple payloads), and implement the reducer using Examples (handler chaining and same-reference no-ops). Do not create migration-local copies of those APIs. Then migrate JSX/TSX consumers with After: direct selector signal plus Store dispatch.

Core implementation contracts

  • State placement is decided by When to use Redux vs component-local state.
  • Pure immutable transitions and no-op identity follow Do and Don't; components dispatch rather than owning the transition.
  • Every migrated field, including nested values and objects previously held by React providers, must meet Do and the complete Don't serialization exclusions. Do not substitute a shorter migration-specific list.
  • Add/update serialization tests for initial state and changed reducer paths according to Verification cues and JSON round-trip regression test. Verify reducer transitions and no-op identity with Verification cues.

Component-local state remains local

import * as React from "react";

export function ProductCard() {
  const [isHovered, setHovered] = React.useState(false);
  return <article onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} />;
}

Bad: reducer side effect

Do not carry a provider's persistence into its replacement reducer. The forbidden operations remain in Don't; migrate that work separately via Conversion recipes.

Cross-references

  • ../../../core/actions/SKILL.md — action creators.
  • ../../../core/reducers/SKILL.md — reducer patterns.
  • ../../../core/state-serialization/SKILL.md — serializable state rules.