TL;DR: A reducer is a tiny pure function that folds many things into one. Use reduce() to transform arrays (sum, group, map→object), and use state reducers (e.g., React’s useReducer) when your UI state transitions need clearer rules. Reducers make code predictable, testable, and easy to reason about.
Table of contents
- The human-friendly definition
- Why bother with reducers?
Array.prototype.reduce()—the pocket guide- Real problems, tiny reducers
- State reducers (React & friends)
- Best practices
- Common gotchas
- Keep learning
- FAQ
The human-friendly definition
Think of a reducer like packing a suitcase: you start with an empty bag (accumulator), pick up one item at a time (current value), decide where it goes, zip the bag, and move on. At the end, you have one neatly packed suitcase.
// (accumulator, current) -> nextAccumulator
const sum = (acc, n) => acc + n;
const total = [2, 5, 7].reduce(sum, 0); // 14
You’ll meet reducers in two places: (1) array reducers with Array.prototype.reduce() and (2) state reducers (React useReducer, Redux) that turn (state, action) into next state.
Why bother with reducers?
- Clarity: Many steps → one pass → one final thing.
- Predictability: Pure functions (no hidden side effects) are easier to test.
- Flexibility: The “one final thing” can be a number, object, Map, Set—whatever you need.
- Great with TypeScript: Lock in the shape of your accumulator and stop guessing.
Array.prototype.reduce()—the pocket guide
array.reduce((accumulator, currentValue, index, array) => {
// return next accumulator
}, initialValue);
Pro tips: Always provide an initialValue (your future self and empty arrays will thank you), and remember the accumulator can be any type or shape.
Real problems, tiny reducers
1) Quick math: sum + max
const nums = [4, 10, 6];
const total = nums.reduce((acc, n) => acc + n, 0);
const max = nums.reduce((acc, n) => Math.max(acc, n), -Infinity);
2) Build a lookup map (id → object)
const users = [{ id:1, name:'A' }, { id:2, name:'B' }];
const byId = users.reduce((acc, u) => {
acc[u.id] = u;
return acc;
}, {});
// byId[2] -> { id:2, name:'B' }
3) Group items (e.g., by category)
const groupBy = (arr, key) =>
arr.reduce((acc, item) => {
const k = item[key];
(acc[k] ??= []).push(item);
return acc;
}, {});
4) De-duplicate while preserving order
const unique = arr =>
arr.reduce((acc, x) => (acc.includes(x) ? acc : acc.concat(x)), []);
5) Filter + map + total in one pass
const totalAfterDiscount = items.reduce((acc, item) => {
if (!item.enabled) return acc; // filter
return acc + item.price * 0.9; // map + reduce
}, 0);
New to fundamentals like scope and closures? Start here: Understanding JavaScript Scope. Async flows pair nicely with reducers: JavaScript Promise Basics.
State reducers (React & friends): same idea, bigger payoff
A state reducer still takes two inputs and returns one output—but now it’s (state, action) → nextState. That’s it.
const initial = { count: 0 };
function counter(state = initial, action) {
switch (action.type) {
case 'inc': return { ...state, count: state.count + 1 };
case 'dec': return { ...state, count: state.count - 1 };
default: return state;
}
}
React’s useReducer in practice
import { useReducer } from 'react';
function counter(state, action) {
switch (action.type) {
case 'inc': return { ...state, count: state.count + 1 };
case 'dec': return { ...state, count: state.count - 1 };
default: return state;
}
}
export default function Counter() {
const [state, dispatch] = useReducer(counter, { count: 0 });
return (
<div>
<button onClick={() => dispatch({ type: 'dec' })}>–</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'inc' })}>+</button>
</div>
);
}
When to reach for useReducer over useState? Use it when multiple related fields change together, you want explicit transitions (e.g., idle → loading → success/error), or you need undo/redo.
Docs: React — useReducer • Redux — Structuring Reducers
Best practices (that save headaches)
- Keep it pure: No fetching, no DOM—just inputs → output.
- Set
initialValue: Especially for array reducers. - Split big reducers: Small, focused reducers are easier to test and reuse.
- Immutability helpers: Try Immer for concise updates.
- Type it (TS): Declare accumulator/state shapes to catch errors early.
type Cart = { items: { id: string; qty: number }[] };
const add = (cart: Cart, id: string): Cart => ({
...cart,
items: cart.items.some(i => i.id === id)
? cart.items.map(i => i.id === id ? { ...i, qty: i.qty + 1 } : i)
: cart.items.concat({ id, qty: 1 })
});
Common gotchas
- Forgetting
initialValueand getting weird edge cases with empty arrays. - Returning the wrong type mid-reduce (start as a number, return an object later).
- Mutating the accumulator when you intended immutability—be consistent.
- Turning the reducer into a kitchen sink—split into helpers and compose.
Keep learning (internal + external links)
- On my blog: Understanding JavaScript Scope • JavaScript Promise Basics • Evolution of Front-End Development (2010–2025)
- Authoritative: MDN: reduce() • React docs: useReducer • Redux: Structuring Reducers • Immer
FAQ
Are reducers faster than multiple loops?
Not automatically. Reducers are about clarity and composition, not guaranteed speed. Profile if performance matters.
Should I always use reducers for state?
No. For simple UI, useState is great. Use a reducer when transitions are complex or you need predictable updates.
Is reduce() the same as a Redux reducer?
They share the fold idea (many → one). reduce() folds array items; Redux/React reducers fold actions into state.
Leave a Reply