Actions and reducers
You've heard the words — action, reducer, operation. This lesson makes the contract precise, so you can reason clearly about what's allowed inside each one.
Actions are just intent
An action is plain data. Nothing more.
{
type: "ADD_TODO",
input: { id: "t-1", title: "Buy milk" }
}
No timestamp. No hash. No index. An action describes what you want to happen — it's a request, not a fact. The action alone changes nothing.
That's intentional. Separating intent from result means you can validate, replay, or reject a change before it ever touches state.
Reducers define the only legal transitions
A reducer is a pure function: (state, action) => nextState. It's the single place where state is allowed to change, and it has strict rules:
- Same inputs must always produce the same output.
- No network calls, no randomness, no wall-clock time.
- Synchronous only — no
async, no promises.
// Valid reducer — pure transformation
addTodoOperation(state, action) {
state.todos.push({
id: action.input.id,
title: action.input.title,
done: false,
});
}
Why so strict? Because peers reconstruct state by replaying the operation log. If a reducer reads Date.now() or calls Math.random(), two machines replaying the same log end up with different state. The whole system breaks.
Non-deterministic values belong in the action input — generated before dispatch, outside the reducer:
// In the editor — generate the ID before dispatching
dispatch(
addTodo({
id: generateId(),
createdAt: new Date().toISOString(),
title: input.title,
}),
);
Operations are what gets recorded
Once the reducer runs without throwing, the Reactor stamps the action with metadata and appends it to the document log. That record is an operation:
{
index: 42,
timestamp: "2026-04-15T10:00:00.000Z",
hash: "sha256-...",
action: { type: "ADD_TODO", input: { id: "t-1", title: "Buy milk" } },
error: null,
}
The operation is immutable history. The action was intent; the operation is fact.
If the reducer throws, the operation is still appended — but with error set to the message and state left unchanged. Failure is also part of the record.
Intent vs. result
The action/reducer/operation triangle gives you a clean separation:
| Carries | |
|---|---|
| Action | Intent — a name and inputs, nothing else |
| Reducer | Logic — the only legal way state can change |
| Operation | Result — the action plus proof it ran (index, hash, timestamp) |
A caller can't sneak state changes past the reducer. A reducer can't introduce uncertainty. An operation can't be revised after the fact. Each piece does exactly one job.