core/actions
Use when creating Themis actions with createAction or createAsyncAction, including tuple payloads, async request promises, and action ownership.
SKILL.mdskills/core/actions/SKILL.md
GitHubActions — createAction / createAsyncAction
Operational guidance for action creator work. API details and longer examples live in
@themislib/themis/docs/REDUCERS.md→ Actions and Async Actions. Public API:@themislib/themis/utils/store/create-action; related reducer guidance: Do.
Use when
- Adding or reusing slice action creators.
- Wiring async request/success/failure flows to reducers or sagas.
- Checking action type ownership during state-integrity work.
Do
- Search first for the intended action type, creator names, and operation terms; one action type string has one canonical owner.
- Namespace action types as
sliceName/actionName. - Use tuple payload typing for positional args, e.g.
createAction<[id: string]>(...). - Use a payload modifier only when callers should pass positional args but reducers/sagas need a shaped payload.
- Pass action creators directly to
takeEvery/takeLatest; theirtoString()exposes the action type.
Async action cues
createAsyncAction<[Args], Success>(asyncType, stagesType)creates the request creator plus static.successand.failurecreators.- A dispatched request action carries
payload,promise, and per-instancesuccess/failurecreators. - Themis internally observes ignored async-action rejections, preventing unhandled-rejection events without changing the original promise; explicit awaiters still receive the original rejection.
- Prefer
try/catcharoundawait store.dispatch(asyncAction(...))when handling a result or failure; dispatch returns the request's original, typed promise. - Reducers normally handle the request creator,
.success, and.failureto update loading/data/error fields. - Sagas watch the request creator unless they are intentionally reacting to success/failure events.
- Only the request instance's
action.success(result)/action.failure(error)settles its promise; static stages merely create Redux actions. Dispatch the instance stage as well when reducers need the update. - Settlement is an application policy, not automatic:
takeLatestcancellation entersfinally, notcatch. Reject cancelled requests explicitly as below. WithtakeLeading, an ignored request never reaches a worker: use ordinarycreateActiontriggers when no response is guaranteed, or an explicit admission owner that rejects ignored requests. Do not await a promise-bearing request when its watcher is absent/stopped.
Examples
No-payload action for explicit events
import { createAction, createAsyncAction } from "@themislib/themis/utils/store/create-action";
export const resetTodos = createAction("todos/reset");
const resetAction = resetTodos();
// Current runtime payload is [], although the no-argument overload says undefined.
// Handle the event by its type; do not branch on payload === undefined.
const refreshTodos = createAsyncAction<void>("todos/refreshAsync", "todos/refresh");
const refreshAction = refreshTodos(); // same [] runtime / undefined type discrepancy
const resetTuple = createAction<[]>("todos/resetTuple")(); // typed and runtime []
const resetUndefined = createAction("todos/resetUndefined", () => undefined)();
The async example explicitly selects the response-type overload (<void>), which declares undefined payload; with no type arguments the current overload order can instead infer any[]. Both produce runtime [] without a modifier. If shape matters, choose an explicit contract: createAction<[]>("todos/reset") gives a typed empty tuple; createAction("todos/reset", () => undefined) intentionally produces runtime undefined. These are existing API limitations, not a reason to change the public API during a guidance fix.
Tuple payload action consumed by reducers
import { createAction } from "@themislib/themis/utils/store/create-action";
import { createReducer } from "@themislib/themis/utils/store/create-reducer";
const renameTodo = createAction<[id: string, title: string]>("todos/rename");
const reducer = createReducer({ titles: {} as Record<string, string> }).with(
renameTodo,
(state, { payload: [id, title] }) => ({ ...state, titles: { ...state.titles, [id]: title } })
);
Payload modifier when reducers need a named object
import { createAction } from "@themislib/themis/utils/store/create-action";
export const renameTodo = createAction(
"todos/rename",
(id: string, title: string, requestedAtMs: number) => ({ id, title, requestedAtMs })
);
const action = renameTodo("todo-1", "Ship docs", Date.now());
action.payload.id satisfies string;
Async request/success/failure triplet
import { createAsyncAction } from "@themislib/themis/utils/store/create-action";
type Todo = { id: string; title: string };
export const loadTodo = createAsyncAction<[id: string], { id: string }, Todo>(
"todos/loadAsync",
"todos/load",
(id) => ({ id })
);
const request = loadTodo("todo-1");
const success = request.success({ id: "todo-1", title: "Ship docs" });
success.payload.request.id satisfies string;
Await dispatch when the caller needs the result
Using loadTodo above and an initialized Themis store whose saga settles the request:
try {
const todo = await store.dispatch(loadTodo("todo-1"));
todo satisfies Todo;
} catch (error) {
console.error("Unable to load todo", error);
}
Watch request creators directly in sagas
import { call, cancelled, put, takeLatest } from "typed-redux-saga";
import { createAsyncAction } from "@themislib/themis/utils/store/create-action";
import { fetchTodo } from "./todos-api";
type Todo = { id: string; title: string };
const loadTodo = createAsyncAction<[id: string], { id: string }, Todo>(
"todos/loadAsync",
"todos/load",
(id) => ({ id })
);
function* watchTodos() {
yield* takeLatest(loadTodo, function* loadTodoWorker(action) {
try {
const todo = yield* call(fetchTodo, action.payload.id);
yield* put(action.success(todo));
} catch (error) {
yield* put(action.failure(error instanceof Error ? error : new Error(String(error))));
} finally {
if (yield* cancelled()) {
yield* put(action.failure(new Error("Todo request cancelled")));
}
}
});
}
Policy here: supersession and owner teardown reject the cancelled request and emit its failure stage. Keep reducer updates correlated with payload.request if older cancellation/failure actions could overwrite newer results. Saga cancellation does not abort an arbitrary Promise/API operation; supply a source-specific abort policy when needed.
❌ Bad: duplicate owner plus tuple/object payload drift
import { createAction } from "@themislib/themis/utils/store/create-action";
type RenameTodoPayload = { id: string; title: string };
// BAD: two modules now claim the same action type, and callers get a tuple-wrapped object.
export const renameTodoFromList = createAction<[payload: RenameTodoPayload]>("todos/rename");
export const renameTodoFromDetail = createAction<[payload: RenameTodoPayload]>("todos/rename");
const [{ id, title }] = renameTodoFromList({ id: "todo-1", title: "Ship docs" }).payload;
Don't
- Do not create aliases with the same type string in another module.
- Do not pass
.typeto saga watchers; pass the creator. - Do not assume cancellation, ignored requests, or static success/failure stages settle an instance promise.
- Do not use object payload types for one-argument actions unless an existing public contract already requires that shape.
- Do not place generated timestamps or IDs in reducers; generate them before dispatch.
- Do not attach
action.promise.catch(...)to Themis async actions, including defensiveaction.promise.catch(() => undefined): it is redundant and reported byredundant-async-action-catchwhen the promise is statically proven to come from a Themis async action. Use the dispatch-await pattern above for caller-owned error handling.
Verification cues
- Reducer tests cover each action handler and no-op reference equality.
- Saga tests prove request watchers receive the request creator, not an accidental success/failure creator.
- State-integrity handoff names the canonical owner or states that no new action owner was added.
See also
@themislib/themis/docs/REDUCERS.md— human reference for action and async-action examples.core/reducers/SKILL.md— consuming actions in.with()handlers.core/sagas/SKILL.md— watcher patterns and typed-redux-saga usage.core/state-integrity/SKILL.md— duplicate-owner search protocol.core/file-structure/SKILL.md— where action creators belong.