Action Creators, Payload, and the Subscribe Pattern

Four patterns that complete pure Redux before you touch React: subscribe for automatic state listening, payload for dynamic data, action type constants, and action creator functions.

June 27, 20265 min read5 / 5

The previous posts built a working Redux store. Every dispatch was hardcoded. Every action carried no data. Every state change was manually printed after each call.

None of that survives contact with a real application.

This post covers the four patterns that close those gaps before Redux connects to actual React components.

[!TIP] Run this yourself: The complete example from this post — action type constants, action creators, subscribe, and payload — is in the code-practice repo. Run node 01-redux-core/index.js to see every state change printed live.

Subscribe: Automatic State Listening

Right now, seeing what changed after a dispatch means writing console.log(store.getState()) after every single call. That is noise that scales badly.

store.subscribe registers a callback that runs automatically after every state change:

JavaScript
store.subscribe(() => { console.log('Updated state:', store.getState()) }) store.dispatch({ type: 'ANSWER_CORRECT' }) // Updated state: 10 store.dispatch({ type: 'ANSWER_CORRECT' }) // Updated state: 20 store.dispatch({ type: 'ANSWER_WRONG' }) // Updated state: 15

The callback fires once per dispatch call, after the reducer returns the new state. You do not need to trigger it manually. The store handles it.

Why this matters: subscribe is the primitive that lets UI frameworks know to re-render. React-Redux's useSelector is built on top of this mechanism. Understanding subscribe explains how the connection between Redux state and React components actually works.

Payload: Sending Data with Actions

Fixed amounts hardcoded into reducer cases are a toy constraint. Real apps need dynamic data.

The payload field carries that data. It is just a property on the action object. There is nothing special about the name payload except that it is the industry-standard convention:

JavaScript
store.dispatch({ type: 'ANSWER_CORRECT', payload: { points: 20 } }) store.dispatch({ type: 'ANSWER_CORRECT', payload: { points: 5 } }) store.dispatch({ type: 'ANSWER_WRONG', payload: { points: 10 } })

The reducer receives the full action object as its second argument. Whatever you put in payload is available via action.payload:

JavaScript
function scoreReducer(state = 0, action) { switch (action.type) { case 'ANSWER_CORRECT': return state + action.payload.points case 'ANSWER_WRONG': return state - action.payload.points default: return state } }

The state progression for those three dispatches: 0 → 20 → 25 → 15.

payload can be any value: a number, a string, an object. Whatever the reducer needs to compute the next state.

Action Type Constants: No More Typos

Every dispatch and every switch case currently uses the same hardcoded string: 'ANSWER_CORRECT', 'ANSWER_WRONG'. The problem is that strings are invisible to the compiler.

A typo like 'ANSWER_CORREKT' in a dispatch call will not throw an error. The action just silently falls through to the default case and nothing happens. These bugs are painful to trace.

The fix: declare each action type as a constant and reference the constant everywhere:

JavaScript
const ANSWER_CORRECT = 'ANSWER_CORRECT' const ANSWER_WRONG = 'ANSWER_WRONG' function scoreReducer(state = 0, action) { switch (action.type) { case ANSWER_CORRECT: return state + action.payload.points case ANSWER_WRONG: return state - action.payload.points default: return state } } store.dispatch({ type: ANSWER_CORRECT, payload: { points: 20 } }) store.dispatch({ type: ANSWER_WRONG, payload: { points: 10 } })

Now if you rename the constant, say from ANSWER_CORRECT to CORRECT_ANSWER, every reference updates in one place. The switch case, every dispatch call, all of it. The string value can change independently of the constant name without touching anything else.

Name the constant in UPPER_SNAKE_CASE. That is the naming convention for action types across the entire Redux ecosystem.

Action Creators: Functions That Build Actions

Dispatching action objects inline has a reliability problem: you have to remember every required property every time. Miss payload, or forget a field inside it, and the reducer silently does the wrong thing.

An action creator is a function that builds and returns the action object. You call the function instead of constructing the object inline:

JavaScript
function answerCorrect(points) { return { type: ANSWER_CORRECT, payload: { points }, } } function answerWrong(points) { return { type: ANSWER_WRONG, payload: { points }, } }

Now dispatch looks like this:

JavaScript
store.dispatch(answerCorrect(20)) store.dispatch(answerCorrect(5)) store.dispatch(answerWrong(10))

The function accepts the dynamic data as a parameter and places it correctly in the payload. The action type is set once inside the function. You cannot dispatch this action with a missing type or a misspelled payload field because the function handles the construction.

The guarantee: anyone calling answerCorrect(20) always gets back a correctly shaped action. The shape is defined once. All callers benefit automatically.

Redux action creator flow ExpandRedux action creator flow

Everything Together

Here is the complete pattern: subscribe, payload, action type constants, action creators:

JavaScript
import { createStore } from 'redux' const ANSWER_CORRECT = 'ANSWER_CORRECT' const ANSWER_WRONG = 'ANSWER_WRONG' function answerCorrect(points) { return { type: ANSWER_CORRECT, payload: { points } } } function answerWrong(points) { return { type: ANSWER_WRONG, payload: { points } } } function scoreReducer(state = 0, action) { switch (action.type) { case ANSWER_CORRECT: return state + action.payload.points case ANSWER_WRONG: return state - action.payload.points default: return state } } const store = createStore(scoreReducer) store.subscribe(() => console.log('Score:', store.getState())) store.dispatch(answerCorrect(20)) // Score: 20 store.dispatch(answerCorrect(5)) // Score: 25 store.dispatch(answerWrong(10)) // Score: 15

This is complete, idiomatic, pre-toolkit Redux. The data flow cycle (action, dispatch, store, reducer, new state) is the same. Action creators and payload are just the practical layer that makes that cycle safe and maintainable at scale.

Next: connecting this Redux store to actual React components using react-redux.

The Essentials

  1. store.subscribe(callback) fires the callback automatically after every state change. It is the primitive that React-Redux builds on to re-render components.
  2. payload is the convention for carrying dynamic data in an action object. The reducer reads it via action.payload.
  3. Action type constants (UPPER_SNAKE_CASE) prevent silent string typo bugs. Reference the constant everywhere: in the reducer and in every dispatch call.
  4. Action creators are functions that build and return action objects. They enforce the correct shape in one place so every caller is guaranteed a valid action.

Further Reading and Watching

Practice what you just read.

Action Creator Patterns — QuizBuild Action Creators — Quiz Score Tracker
2 exercises