| name | core/state-serialization |
| description | Redux state must be structured-cloneable. Allowed: string, number, boolean, null, undefined, plain objects, arrays of primitives. Forbidden: Date, Map, Set, WeakMap, WeakSet, RegExp, Promise, Function, class instances, Error, Symbol. Alternatives: Date → number (ms), Map → Record<string, T>, Set → string[] or Record<string, true>, RegExp → string, Error → { message, stack }. |
| type | sub-skill |
| requires | ["core"] |
| triggers | ["serializable state","structured clone","no date in state"] |
State Serialization
Operational checklist for reducer state shape. Full rationale and examples: docs/REDUCERS.md → State Serialization Rules. Reducer API: @augmentcode/themis/utils/store/create-reducer; related guidance: ../SKILL.md §12.
Use when
- Adding or changing reducer state, initial state, persisted state, or action payloads that become state.
- Reviewing helpers that introduce dates, errors, maps/sets, class instances, functions, promises, or browser APIs.
Do
- Keep Redux state JSON-shaped: primitives, arrays, and plain objects.
- Store timestamps as
number milliseconds or ISO strings; generate them in action creators or sagas, not reducers.
- Store entity sets/maps as
Record<string, T>, string[], or Collection<T, K>.
- Store errors as plain data such as
{ message: string; stack?: string }.
- Add or update serialization tests for new reducer state: stringify/parse should round-trip the state.
Don't
- Do not store
Date, Map, Set, WeakMap, WeakSet, RegExp, Promise, Function, class instances, Error, or Symbol values.
- Do not call
Date.now(), Math.random(), or ID generators inside reducer handlers; pass generated values in the action payload.
- Do not hide non-serializable objects inside collections or nested state fields.
Examples
Plain JSON-shaped state
type Todo = { id: string; title: string; completed: boolean };
type TodosState = {
ids: string[];
byId: Record<string, Todo>;
loading: boolean;
selectedId: string | null;
};
Store timestamps as numbers or ISO strings
import { createAction } from "@augmentcode/themis/utils/store/create-action";
import { createReducer } from "@augmentcode/themis/utils/store/create-reducer";
const markSynced = createAction("todos/markSynced", (syncedAtMs: number, syncedAtIso: string) => ({ syncedAtMs, syncedAtIso }));
export const reducer = createReducer({ syncedAtMs: 0, syncedAtIso: "" }).with(markSynced, (state, { payload }) => ({
...state,
syncedAtMs: payload.syncedAtMs,
syncedAtIso: payload.syncedAtIso,
}));
Serialize errors before storing them
type SerializableError = { message: string; stack?: string };
function toSerializableError(error: unknown): SerializableError {
if (error instanceof Error) return { message: error.message, stack: error.stack };
return { message: String(error) };
}
const failedState = { error: toSerializableError(new Error("network failed")) };
Use records or collections instead of Map and Set
import { type Collection, createCollection } from "@augmentcode/themis/utils/collections/collection-utils";
type Todo = { id: string; title: string };
type TodosState = {
todos: Collection<Todo, "id">;
selectedById: Record<string, true>;
};
const initialState: TodosState = { todos: createCollection<Todo, "id">("id"), selectedById: {} };
JSON round-trip regression test
import { describe, expect, it } from "vitest";
const todosReducer = {
initialState: { ids: [] as string[], byId: {} as Record<string, { id: string; title: string }> },
};
describe("todos state serialization", () => {
it("round-trips through JSON", () => {
const state = todosReducer.initialState;
expect(JSON.parse(JSON.stringify(state))).toEqual(state);
});
});
❌ Bad: non-serializable state and reducer-created values
import { createAction } from "@augmentcode/themis/utils/store/create-action";
import { createReducer } from "@augmentcode/themis/utils/store/create-reducer";
type Todo = { id: string; title: string };
type TodosState = { updatedAt: Date; byId: Map<string, Todo>; selectedIds: Set<string>; error?: Error };
const initialState: TodosState = { updatedAt: new Date(0), byId: new Map(), selectedIds: new Set() };
const markTouched = createAction("todos/markTouched");
export const reducer = createReducer<TodosState>(initialState).with(markTouched, (state) => ({
...state,
updatedAt: new Date(),
error: new Error("changed"),
}));
Implementation cues
- If a value needs behavior, store its data in Redux and run behavior in a saga/service.
- If a value is derived from state, keep it out of state and expose it through selectors.
- If a collection-like field grows beyond a simple primitive list, prefer
Collection<T, K> and the collection helpers.
Verification cues
- Reducer tests cover initial state and changed paths with
JSON.stringify/JSON.parse round-trips.
- Search new state types for forbidden constructors and browser objects before handoff.
- Run the repository's skill/example validation when documentation examples changed.
See also
docs/REDUCERS.md — human reference for serialization rules and examples.
core/reducers/SKILL.md — reducer purity and same-reference no-op behavior.
core/collections/SKILL.md — serializable normalized entity storage.
core/state-integrity/SKILL.md — derived values and canonical ownership.