skip to content

Actions and Reducers

Actions describe what happened and reducers decide the resulting state, with both required to stay pure and immutable. Expect questions on why mutating state breaks Redux and how combineReducers splits one store across features.

part ofReduxoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Redux, what is an action, what is an action creator, and why write action creators instead of inline action objects?

level: juniorimportance: must knowfreq 72%

answer

  1. a plain object describing what happened
  2. type is required, and a string
  3. payload, meta, error
  4. a function that returns the object

basics

~20 s

An action is a plain object with a string type that describes something that happened, usually with its data in payload; an action creator is a function that builds that object, so every dispatch site produces the same, correctly shaped action.

solid answer

~40 s

An action is the only input a Redux store accepts: a plain object whose `type` names an event, such as `'todos/todoAdded'`, plus any data it needs, conventionally in `payload` (the Flux Standard Action shape also allows `meta` and `error`). In Redux 5, `dispatch` throws if the action is not a plain object or if `type` is not a string. An action creator is just a function that returns an action: `todoAdded(text)` returns `{ type: 'todos/todoAdded', payload: { id, text } }`. Creators keep the type string and payload shape in one place, give you a typed signature, and are where you prepare non-deterministic data such as generated IDs or timestamps, which a pure reducer must not create. Redux Toolkit generates them for you, but the idea is the same.

code

ts · 16 lines
ts
export const TODO_ADDED = 'todos/todoAdded'

export interface TodoAddedAction {
  type: typeof TODO_ADDED
  payload: { id: string; text: string; createdAt: number }
}

// Impure preparation (ID, clock) happens here, before dispatch
export function todoAdded(text: string): TodoAddedAction {
  return {
    type: TODO_ADDED,
    payload: { id: crypto.randomUUID(), text, createdAt: Date.now() },
  }
}

// dispatch(todoAdded('Buy milk'))

go deeper

for a junior

Define an action as a plain object with a string type and a payload, and an action creator as a function that returns one.

for a middle

Explain why impure preparation such as IDs or timestamps belongs in the creator, and what Redux 5 enforces on the type field.

for a senior

Argue for event-style actions over setters: fewer dispatches, consistent updates and a readable log, and show how that shapes reducer design.

for a principal

Treat the action log as a product artefact: naming conventions and payload contracts that make debugging, analytics and replay workable across teams.

## Actions: events, described as data In Redux, nothing changes state except an **action** passed to `dispatch`. An action is a **plain JavaScript object** that describes *something that happened* in the app: ```js { type: 'todos/todoAdded', payload: { id: 'a1', text: 'Buy milk' } } ``` The rules come from the core and from convention: - **Plain object.** The core `dispatch` throws "Actions must be plain objects" for anything else: a function, a Promise or a class instance. Middleware can accept other values, but whatever finally reaches the reducer must be a plain object. - **`type` is required and must be a string.** Redux 5 throws if `type` is `undefined` and also if it is not a string. Redux 4 accepted any defined value, such as a Symbol or a number; Redux 5 made strings mandatory so actions stay serializable and readable in the DevTools log. - **Everything else is up to you**, but the **Flux Standard Action** convention, which the Redux style guide recommends, says: put the data in `payload`, optional extra information in `meta`, and flag failures with `error`. ### Naming the type The style guide suggests `'domain/eventName'` strings such as `'todos/todoAdded'`. The type is what reducers switch on and what you read in the action history, so it should say **what happened**, not which field to set. ## Events, not setters A good action describes an event (`'cart/checkoutSubmitted'`) rather than a command to set a field (`'cart/setIsSubmitting'`). The difference matters: | Setter-style actions | Event-style actions | |---|---| | Caller must know the state shape | Caller only says what happened | | Several dispatches for one user event | One dispatch, many reducers may respond | | Log reads like assignments | Log reads like a story of the session | | Intermediate states between dispatches | One consistent update | ## Action creators An **action creator** is a function that returns an action: ```ts const todoAdded = (text: string) => ({ type: 'todos/todoAdded', payload: { id: crypto.randomUUID(), text }, }) ``` Nothing in Redux requires them; `dispatch({ type: 'todos/todoAdded', payload })` works. Teams use them because: 1. **One source of truth for the shape.** The type string and payload structure live in one function instead of being retyped at every dispatch site, where a typo in a type string silently matches no reducer. 2. **Preparation logic has a home.** Generating an ID, a timestamp or a normalized payload is **impure** work. It cannot go in the reducer, which must return the same output for the same input, so it belongs in the creator, before dispatch. 3. **Types.** A creator gives TypeScript a single place to declare the action type, so reducers and dispatch sites agree. 4. **Readability.** `dispatch(todoAdded(text))` reads as intent. Redux also ships `bindActionCreators`, which wraps creators so that calling them dispatches immediately. It is rarely needed with hooks, but it shows the idea: a creator only **builds** an action; dispatching is a separate step. ## Action creators are not thunks An action creator returns a **plain object** and has no side effects apart from preparation. A function that returns **another function** to be run by middleware is a *thunk action creator*, a different tool for async logic. Mixing the terms is a common interview slip. ## Judging actions | Action | Verdict | Why | |---|---|---| | `{ type: 'todos/todoAdded', payload: { id, text } }` | Good | Event name, data in `payload`, serializable | | `{ type: 'SET_DATA', payload }` | Legal but weak | Generic setter; the log says nothing about what happened | | `{ type: Symbol('added') }` | Throws in Redux 5 | `type` must be a string | | `{ type: 'user/loggedIn', payload: new User(json) }` | Avoid | Class instances are not serializable and may mutate in place | | `() => fetchTodos()` | Throws without middleware | Not a plain object; needs the thunk middleware | Two quick checks catch most mistakes. First, **could you log it and replay it tomorrow?** If the action holds a function, a Promise or a class instance, the answer is no. Second, **does the type read like a past-tense event?** If it reads like an assignment, consider whether one event-style action could replace several setters. ## Modern practice In Redux Toolkit, `createSlice` generates the type strings and action creators from your reducer names, and each creator exposes its `type`. The underlying object is still a plain action with `type` and `payload`, so everything above explains what the generated code does.

  • Why must a generated ID be created in the action creator rather than in the reducer?
    A reducer must be pure: the same state and action must always produce the same result. Replaying recorded actions, running tests or time-travel debugging would otherwise create different IDs each time. Generating the ID before dispatch puts it inside the action, so every replay reproduces the same state.
  • Why does the Redux style guide prefer actions that describe events over actions that set fields?
    An event such as 'cart/checkoutSubmitted' lets any number of reducers respond in one dispatch, keeps callers ignorant of the state shape, and produces a log that reads as what the user did. Setter actions push state knowledge into callers, need several dispatches per user event and create intermediate states between them.

saying these in an interview costs you the question

  • An action is a function that updates the state
  • Any value can be an action type in Redux 5, including Symbols
  • Action creators dispatch the action themselves
  • Random IDs should be generated inside the reducer
  • Action types should name the field being set, like SET_NAME
open as a page

What contract must a Redux reducer honour for undefined state, unknown actions and side effects, and why does Redux depend on it?

level: middleimportance: must knowfreq 68%

basics

~20 s

A Redux reducer takes (state, action) and returns the next state: its initial state when given undefined, the same state for actions it ignores, never undefined, and without side effects, so replaying actions always reproduces the same state.

open as a page

A Redux reducer runs `state.lists[id].items.push(item)` and returns `{ ...state }`, yet the list component never re-renders. Why, and how do you fix it?

level: seniorimportance: must knowfreq 60%

basics

~20 s

The spread copies only the root; lists, the list and its items array keep their references, and push mutated that array in place, so a selector returns the same array and the component skips rendering. Copy every level on the path.

open as a page

How does Redux's combineReducers compute the next state on each dispatch, and when does it return the previous root object unchanged?

level: middleimportance: should knowfreq 55%

basics

~10 s

combineReducers calls every slice reducer with its own slice and the same action, collects the results under the same keys, and returns the previous root object when every slice returned its previous reference.

open as a page

Why does Redux recommend storing relational data in a normalized shape with byId and allIds, and how does that change reducer code?

level: middleimportance: should knowfreq 48%

basics

~20 s

Normalizing stores each entity type once, in a byId lookup plus an allIds order array, with relations held as IDs, so each record has one copy, updates touch one short path, and unrelated components keep their references.

open as a page