Actions (Saga template)
What are Actions?
Actions are plain JavaScript objects describing "what happened"—not how state should change, just that a user clicked submit or an API resolved. They are the only source of information for the store; in the saga template they are also the only way the UI talks to the saga middleware (no thunks, no async functions inside dispatch).
Key Principles
Three phases per async op. Every async CRUD verb gets exactly three constants and three creators:
<OP>_<ENTITY>(REQUEST),<OP>_<ENTITY>_SUCCESS,<OP>_<ENTITY>_ERROR. The container dispatches the bare REQUEST; the sagaputs the SUCCESS or ERROR.One phase for sync ops. Stage-style ops (e.g.
EDIT_TODO, which only setscurrentTodoItem) emit the bare REQUEST and nothing else. The reducer handles it directly.UPPER_SNAKE_CASE type constants. Every type is declared once in
<slice>.type.jsand imported by both the action creator and the reducer. Prevents typo-driven silent bugs.Plain serializable objects. Action payloads are POJOs only — no functions, Promises, classes, or Date instances. This is what makes redux-saga's effects testable: the saga compares dispatched actions by deep equality.
Hand-written creators, not RTK. This template intentionally does NOT use
createSlice/createAsyncThunk. The saga middleware listens for the REQUEST type directly, so abstracting creator boilerplate would only obscure the action surface.Per-op signature.
create<E>(text)takes the new content and seeds{text, completed:false, id:undefined}.delete<E>(id)takes the id, packs{id}.toggle<E>andupdate<E>take the full payload.read<E>()takes nothing meaningful (often called fromuseEffecton mount). Success/error creators all takepayloadorerror.
Best Practices
✅ DO:
- Co-locate types and creators in the slice folder (
<slice>.type.js,<slice>.actions.js). - Group creators in CRUD-canonical order:
create → read → edit → update → toggle → delete. - Make
<op>Error(error)accept any throwable; convert witherror.toString()at the saga boundary. - For optimistic ops (toggle/delete), have
<op>Error(payload, error)carry the rollback list aspayload.
❌ DON'T:
- Don't put
dispatchcalls inside action creators — creators return objects, full stop. - Don't add async behaviour to creators (no thunks). The saga middleware is the only async seam.
- Don't reuse one action type for multiple ops (
UPDATE_TODOshould not also mean "create a new todo").
Code Patterns
Type constants (<slice>.type.js)
// Three phases per async op…
export const CREATE_TODO = "CREATE_TODO"
export const CREATE_TODO_SUCCESS = "CREATE_TODO_SUCCESS"
export const CREATE_TODO_ERROR = "CREATE_TODO_ERROR"
export const READ_TODO = "READ_TODO"
export const READ_TODO_SUCCESS = "READ_TODO_SUCCESS"
export const READ_TODO_ERROR = "READ_TODO_ERROR"
export const UPDATE_TODO = "UPDATE_TODO"
export const UPDATE_TODO_SUCCESS = "UPDATE_TODO_SUCCESS"
export const UPDATE_TODO_ERROR = "UPDATE_TODO_ERROR"
export const TOGGLE_TODO = "TOGGLE_TODO"
export const TOGGLE_TODO_SUCCESS = "TOGGLE_TODO_SUCCESS"
export const TOGGLE_TODO_ERROR = "TOGGLE_TODO_ERROR"
export const DELETE_TODO = "DELETE_TODO"
export const DELETE_TODO_SUCCESS = "DELETE_TODO_SUCCESS"
export const DELETE_TODO_ERROR = "DELETE_TODO_ERROR"
// …one phase per sync op.
export const EDIT_TODO = "EDIT_TODO"
Action creators (<slice>.actions.js)
import {
CREATE_TODO, CREATE_TODO_SUCCESS, CREATE_TODO_ERROR,
READ_TODO, READ_TODO_SUCCESS, READ_TODO_ERROR,
EDIT_TODO,
UPDATE_TODO, UPDATE_TODO_SUCCESS, UPDATE_TODO_ERROR,
TOGGLE_TODO, TOGGLE_TODO_SUCCESS, TOGGLE_TODO_ERROR,
DELETE_TODO, DELETE_TODO_SUCCESS, DELETE_TODO_ERROR,
} from "./todo.type";
// CREATE: container passes text; saga assigns id; reducer appends on SUCCESS.
export const createTodo = (text) => ({ type: CREATE_TODO, payload: { text, completed: false } });
export const createTodoSuccess = (payload) => ({ type: CREATE_TODO_SUCCESS, payload });
export const createTodoError = (error) => ({ type: CREATE_TODO_ERROR, error });
// READ: container fires on mount; saga fetches; reducer hydrates on SUCCESS.
export const readTodo = (payload) => ({ type: READ_TODO, payload });
export const readTodoSuccess = (payload) => ({ type: READ_TODO_SUCCESS, payload });
export const readTodoError = (error) => ({ type: READ_TODO_ERROR, error });
// EDIT: sync; reducer just sets currentTodoItem.
export const editTodo = (payload) => ({ type: EDIT_TODO, payload });
// UPDATE: optimistic write through; saga PUTs and SUCCESS clears currentTodoItem.
export const updateTodo = (payload) => ({ type: UPDATE_TODO, payload });
export const updateTodoSuccess = (payload) => ({ type: UPDATE_TODO_SUCCESS, payload });
export const updateTodoError = (error) => ({ type: UPDATE_TODO_ERROR, error });
// TOGGLE: optimistic flip; ERROR carries the previous list for rollback.
export const toggleTodo = (payload) => ({ type: TOGGLE_TODO, payload });
export const toggleTodoSuccess = () => ({ type: TOGGLE_TODO_SUCCESS });
export const toggleTodoError = (payload, error) => ({ type: TOGGLE_TODO_ERROR, payload, error });
// DELETE: optimistic remove; same rollback shape as toggle.
export const deleteTodo = (id) => ({ type: DELETE_TODO, payload: { id } });
export const deleteTodoSuccess = () => ({ type: DELETE_TODO_SUCCESS });
export const deleteTodoError = (payload, error) => ({ type: DELETE_TODO_ERROR, payload, error });
Dispatching from a container
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { readTodo, toggleTodo, editTodo, deleteTodo } from "../state/todo/todo.actions";
function TodoListContainer() {
const dispatch = useDispatch();
// READ on mount — saga middleware sees REQUEST, runs getTodos worker.
useEffect(() => { dispatch(readTodo()); }, [dispatch]);
return /* …pass dispatch handlers down to organism… */;
}
Per-op signature cheatsheet
| Op | Request creator | Success creator | Error creator |
|---|---|---|---|
create |
createTodo(text) |
createTodoSuccess(p) |
createTodoError(error) |
read |
readTodo(payload?) |
readTodoSuccess(p) |
readTodoError(error) |
edit |
editTodo(payload) (sync) |
— | — |
update |
updateTodo(payload) |
updateTodoSuccess(p) |
updateTodoError(error) |
toggle |
toggleTodo(payload) |
toggleTodoSuccess() |
toggleTodoError(prevList, err) |
delete |
deleteTodo(id) |
deleteTodoSuccess() |
deleteTodoError(prevList, err) |
FSA-ish — but only type is mandatory
The template doesn't enforce strict FSA; success/error variants either carry payload or carry error (a string), and meta is unused. The contract that matters:
{ type: <STRING_CONST>, payload?: any, error?: string }
Related Terminologies
- Saga — listens for REQUEST types, dispatches SUCCESS/ERROR.
- Reducer — switches on all three phases per op.
- Store — dispatches actions through the saga middleware first, then to reducers.
Quality Gates
- Three constants per async op; one per sync op.
- Creators are pure POJO factories (no
dispatch, no closures over state). - Toggle/delete success creators take no args; error creators carry
(prevList, error). - Test imports use the same name as the constants (no aliasing).
- No
createAsyncThunk/createSlice(those belong to a different archetype).
Source: templates/chota-react-saga/src/state/todo/todo.type.js, todo.actions.js