core/state-serialization
Use when checking Redux state for structured-clone compatibility or replacing non-serializable values such as Date, Map, Set, and Error.
SKILL.mdskills/core/state-serialization/SKILL.md
GitHubState Serialization
Operational checklist for reducer state shape. Full rationale and examples:
@themislib/themis/docs/REDUCERS.md→ State Serialization Rules. Reducer API:@themislib/themis/utils/store/create-reducer; canonical checklist: Do and Don't.
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
numbermilliseconds or ISO strings; generate them in action creators or sagas, not reducers. - Store entity sets/maps as
Record<string, T>,string[], orCollection<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, orSymbolvalues. - 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 "@themislib/themis/utils/store/create-action";
import { createReducer } from "@themislib/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 "@themislib/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 "@themislib/themis/utils/store/create-action";
import { createReducer } from "@themislib/themis/utils/store/create-reducer";
// BAD: Date, Map, Set, Promise, class instances, and Error do not belong in Redux state.
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.parseround-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
@themislib/themis/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.