react/migration/derived-stores
Move shared React useMemo, hook/context derivations, and repeated render calculations to ReactStore selector definitions; route consumption to selector-lifecycle.
SKILL.mdskills/react/migration/derived-stores/SKILL.md
GitHubReact derived state migration
Shared React derivations move to reactStore.createSelector(...). This leaf
maps old calculations to selector definitions; choose consumption and test APIs
using Call-mode map.
React sources include duplicated useMemo, derived custom-hook return values,
context selector helpers, and render-time calculations reused across components.
Before: duplicated React derivation
import * as React from "react";
import { useCartContext } from "./CartProvider";
export function CartSummary() {
const { items, discountCode } = useCartContext();
const subtotal = React.useMemo(() => items.reduce((sum, item) => sum + item.price, 0), [items]);
const total = discountCode ? subtotal * 0.9 : subtotal;
return <span>{total}</span>;
}
After: Store-bound selectors
Migrate cart entity records to a Collection<CartItem, "id"> in the cart
reducer's collection field. Materialize an array only as derived output with
getItems; do not preserve the legacy object array as canonical Redux state.
The shared Collection policy applies to React too; primitive arrays remain valid.
import { reactStore } from "../react-store";
import { getItems } from "@themislib/themis/utils/collections/collection-utils";
export const selectCartItems = reactStore.createSelector((state) => getItems(state.cart.collection));
export const selectDiscountCode = reactStore.createSelector((state) => state.cart.discountCode);
export const selectCartSubtotal = reactStore.createSelector((state) => {
return selectCartItems.select(state).reduce((sum, item) => sum + item.price, 0);
});
export const selectCartTotal = reactStore.createSelector((state) => {
const subtotal = selectCartSubtotal.select(state);
return selectDiscountCode.select(state) ? subtotal * 0.9 : subtotal;
});
Component and test consumption
The migrated read below is signal-aware. Apply
React signal consumption guardrails
for .value tracking and any necessary plain-value fallback boundary.
import { selectCartTotal } from "../store/cart/cart-selectors";
export function CartSummary() {
const total = selectCartTotal();
return <span>{total.value}</span>;
}
import { expect, it } from "vitest";
import { selectCartTotal } from "../store/cart/cart-selectors";
import { reactStore } from "../react-store";
import { createCollection } from "@themislib/themis/utils/collections/collection-utils";
it("selects the cart total from explicit state", () => {
const disposeStore = reactStore.init();
try {
// Read state only after this test initializes its runtime; retain other domains.
const mockState = {
...reactStore.state,
cart: { collection: createCollection("id", [{ id: "a", price: 10 }]), discountCode: null },
};
expect(selectCartTotal.select(mockState)).toBe(10);
} finally {
disposeStore();
}
});
Importing the test only registers it; initialization, state reads, and cleanup
happen inside the test callback. This test owns the configured Store runtime for
its duration, so do not run it concurrently with other tests using that instance.
The .select(mockState) call itself is pure and needs no live runtime if a test
supplies a complete state fixture instead of reading reactStore.state.
Parameterized selectors
export const selectTodoById = reactStore.createSelector((state, id: string) => {
return state.todos.byId[id];
});
export const selectTodoTitle = reactStore.createSelector((state, id: string) => {
return selectTodoById.select(state, id)?.title ?? "Untitled";
});
Rules
- One selector per shared derivation; keep selectors narrow and pure.
- Compose upstream selectors with
.select(state, ...args)inside selector bodies. - Verify migrated consumer boundaries using Verification cues, not a migration-specific call-mode policy.
- Do not store selector outputs in reducers; reducers own base state only.
- Store entity records in Collections and derive arrays at the selector boundary;
follow
../../../core/collections/SKILL.md, not the legacy context shape.
Bad: direct signal form inside selector composition
// BAD: selector callbacks must stay pure and synchronous against the provided state.
export const selectBadTotal = reactStore.createSelector(() => {
return selectCartSubtotal().value;
});
Cross-references
../../selectors/SKILL.md— selector authoring and caching.../../selector-lifecycle/SKILL.md— direct signal,.useValue,.select,.effect, and.withStorechoices.../component-migration/SKILL.md— component consumption after migration.