Testing Reducers in React Without Rendering Components

Test React reducers as pure functions with Vitest: call the reducer with a state and an action, assert the result, and check for mutation.

5 min read

Testing reducers in React without rendering components works because a reducer is a pure function. Call it with a starting state and an action, then assert on the returned state. No DOM, no React renderer, and no test environment beyond a JavaScript runtime are required.

Because the reducer imports nothing from React, the test file runs in milliseconds. A few dozen cases still finish in well under a second.

Why reducers test well

A reducer takes state and an action and returns the next state. Because it has no side effects and no dependencies, you can exercise every branch with two arguments and one assertion per case. Each switch case becomes one test, and a failing test names the exact action that broke.

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

This reducer lives in tasksReducer.js and is exported so the test file can import it. The tests will live in tasksReducer.test.js next to it.

Set up Vitest

Install Vitest as a dev dependency and import its test helpers. Vitest is the common runner in the React ecosystem, and these imports are all you need for plain function tests.

Install it with npm install -D vitest and add a test script that runs vitest run. The same pattern works with any runner because the reducer has no React imports.

index.jsindex.js
import { describe, it, expect } from "vitest";
import { tasksReducer } from "./tasksReducer";
 
describe("tasksReducer", () => {
  it("adds a task", () => {
    const next = tasksReducer([], { type: "added", id: 1, text: "Pack" });
    expect(next).toEqual([{ id: 1, text: "Pack" }]);
  });
});

The first test calls the reducer with an empty list and an added action, then asserts the returned array contains one task. Run it with npx vitest run from the project root.

Check mutation and identity

A reducer must return new objects and arrays, not change the ones it received. Assert both that the original input is unchanged and that the returned value is a different reference.

index.jsindex.js
it("does not mutate the previous state", () => {
  const before = [];
  const next = tasksReducer(before, { type: "added", id: 1, text: "Pack" });
  expect(before).toEqual([]);
  expect(next).not.toBe(before);
});

The before array is still empty after the call, and next is a new array. This test fails if someone adds push to the reducer later. toEqual compares values, while toBe and not.toBe compare references, so one proves correctness and the other proves immutability.

Cover the unknown action

The default branch should return the same state so unknown actions do not crash or wipe data. Identity matters here, not just deep equality.

index.jsindex.js
it("returns the same state for an unknown action", () => {
  const tasks = [{ id: 1, text: "Pack" }];
  expect(tasksReducer(tasks, { type: "unknown" })).toBe(tasks);
});

Using toBe checks that the exact same reference comes back, which means no copy was made. You can also dispatch several actions in a row and assert the final state, which mirrors a real session. Read why reducers must be pure for the reasoning behind these checks, or how to write actions and reducers to design the cases you test.

Rune AI

Rune AI

Key Insights

  • Reducers are pure functions, so they test fast.
  • Call the reducer with state and an action, then assert.
  • Use toEqual for new objects and toBe for identity.
  • Assert the previous state was not mutated.
  • Add a case for unknown actions.
RunePowered by Rune AI

Frequently Asked Questions

Do I need React or jsdom to test a reducer?

No. A reducer is a plain function, so it runs in any JavaScript test runner without a DOM or a component renderer.

Which test runner should I use?

Vitest is the common choice in the React ecosystem. The same pattern works with any runner because the reducer has no React dependencies.

Should I test reducers instead of components?

Test both. Reducer tests cover state logic quickly, while component tests cover what the user sees and does.

Conclusion

Testing a reducer without rendering components is just testing a pure function. Call it with a starting state and an action, assert the returned state, and add checks for mutation and unknown actions.