How to Write Actions and Reducers in React

Learn to write clear React actions and pure reducers: action shape, event-based naming, switch conventions, and the purity rules that keep state predictable.

5 min read

Actions and reducers in React are the two halves of the useReducer pattern. An action is a plain object that describes what the user did, and a reducer is a pure function that returns the next state from the current state and that action. Naming each piece well keeps state transitions easy to trace.

Model actions as events

An action is the smallest description of one interaction. Give it a type string and only the fields the reducer needs to compute the next state.

index.jsindex.js
{ type: "task/added", id: 3, text: "Book a flight" }

The type names the event, while id and text carry the payload. Include only the fields the reducer needs, nothing more. The slash in the name is optional; some teams use it to group actions by feature.

Write a pure reducer

A reducer receives state and an action, then returns the next state. Use a switch, wrap each case in braces, and return from every branch.

index.jsindex.js
function tasksReducer(tasks, action) {
  switch (action.type) {
    case "task/added":
      return [...tasks, { id: action.id, text: action.text }];
    case "task/removed":
      return tasks.filter((task) => task.id !== action.id);
    default:
      return tasks;
  }
}

Each branch builds a new array. Wrapping each case in braces stops variables from leaking between branches, and returning from every case prevents accidental fall-through. The default branch returns the same tasks reference, which tells React there is nothing new to render.

Connect the reducer with useReducer

Dispatch the action from an event handler, and React feeds it to the reducer on the next render.

App.jsxApp.jsx
import { useReducer } from "react";
function TaskList() {
  const [tasks, dispatch] = useReducer(tasksReducer, []);
  return (
    <button onClick={() => dispatch({ type: "task/added", id: 4, text: "Pack" })}>
      Add task
    </button>
  );
}

Clicking the button dispatches one action. The reducer returns a new array with the extra task, and the component re-renders with the updated list.

Walk through one transition

Start with an empty list. Dispatching task/added with id 4 runs the reducer with the current empty array and that action, which returns a new array holding one task. React stores the result and re-renders, so the list shows a single item.

Dispatching task/removed with id 4 next filters the array back to empty.

Name actions after events, not setters

Action names should describe what happened, not what the state should become. A reset button that clears five fields should dispatch one reset action, not five setField actions.

  • Good names: task/added, form/reset, filter/changed
  • Poor names: setTasks, clearName, setEmailToEmpty

When you log every action, the good names reconstruct the user's session in order. See useReducer explained with examples for the dispatch side of this loop.

Keep the reducer pure

A reducer must not call APIs, read the clock, generate random values, or mutate its arguments. The same state and action must always return the same result.

index.jsindex.js
function reducer(state, action) {
  switch (action.type) {
    case "task/added":
      return { ...state, items: [...state.items, action.item] };
    default:
      return state;
  }
}

Returning a new object with a new items array is pure. Pushing into state.items directly would mutate state and hide the update. Read why reducers must be pure for the full reasoning.

Rune AI

Rune AI

Key Insights

  • Actions describe what happened, not the desired state change.
  • A reducer is pure and returns the next state.
  • Return a new object or array instead of mutating.
  • Name actions after events for clearer logs.
  • Keep side effects in handlers, never in reducers.
RunePowered by Rune AI

Frequently Asked Questions

What shape should an action have?

By convention an action is an object with a type string plus the payload fields the reducer needs. Any shape works, but the type property is the common identifier.

Should a reducer use switch or if/else?

Either is correct. switch is the common convention because it lists each case clearly, but if/else produces identical behavior.

Can a reducer dispatch another action?

No. A reducer must stay pure and only return the next state. Triggering another dispatch belongs in an event handler.

Conclusion

A good action names the event that happened, and a good reducer returns the next state without side effects or mutation. Keep actions minimal, name them after user intent, and let the reducer be the single place that decides how state changes.