Context & Reducers — Series 2

Preview — 3 of 10 questions

Why does the default case return state unchanged, rather than throwing an error for an unrecognized action.type?

javascript
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}
AThrowing would actually be the correct, recommended behavior — returning state unchanged is a common beginner mistake that silently hides real bugs
BReturning the same state reference for unrecognized actions lets useReducer's built-in bailout (identical to useState's — comparing the new value to the old one) correctly skip a wasted re-render for an action that doesn't actually change anything; it's also simply safer default behavior — an unexpected action type shouldn't crash the whole app, especially since actions can originate from various dispatchers (including ones that might send an action this particular reducer doesn't handle by design, e.g. a shared action creator used by multiple reducers)
CuseReducer requires the default case to be present syntactically, but its return value is otherwise ignored entirely
DReturning state unchanged is required specifically to satisfy TypeScript's exhaustiveness checking on the action.type union

What does combining useReducer and useContext like this achieve, compared to just using useState + Context with individually-passed setter functions?

javascript
const CartContext = createContext(null);

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });
  return (
    <CartContext.Provider value={{ state, dispatch }}>
      {children}
    </CartContext.Provider>
  );
}

function useCart() {
  return useContext(CartContext);
}

// Anywhere deep in the tree:
function AddToCartButton({ product }) {
  const { dispatch } = useCart();
  return <button onClick={() => dispatch({ type: 'add', product })}>Add</button>;
}
AIt's required — useState setters cannot be passed through Context at all, only dispatch functions can
BIt makes the app render faster overall, since useReducer is inherently more performant than useState when used with Context
CIt only matters for TypeScript type inference — the runtime behavior is identical to useState + Context regardless of which is used
DAny component that needs to trigger a cart change just calls dispatch({ type: '...', ... }) with a plain, serializable action object — the shape of what dispatched updates look like is centralized and consistent (one dispatch function, many possible action types) rather than needing to know about and call several differently-named individual setter functions passed down through the same context; this also makes it easy to add logging/persistence middleware-like behavior by wrapping dispatch in one place

doneTodos is computed fresh (.filter() creates a new array) inside the selector on every single store read. What problem does this cause, and why?

javascript
const useStore = create((set) => ({
  todos: [{ id: 1, text: 'Buy milk', done: false }],
  toggleTodo: (id) => set(state => ({
    todos: state.todos.map(t => t.id === id ? { ...t, done: !t.done } : t)
  })),
}));

function TodoStats() {
  // Selecting a *derived*, freshly-computed array every call:
  const doneTodos = useStore(state => state.todos.filter(t => t.done));
  return <p>{doneTodos.length} done</p>;
}
AThis causes TodoStats to re-render on every store update, even ones completely unrelated to todos' done status — because Zustand's default subscription compares the selector's result by reference (Object.is), and .filter() returns a brand-new array reference every single time the selector runs, so it always looks "changed" to Zustand's comparison, regardless of whether the actual done-items actually differ
BThis throws a runtime error — Zustand selectors are not allowed to return derived/computed values, only direct fields from the store
CThere's no problem at all — Zustand automatically deep-compares selector results by default, so this is already optimal
DThis only causes extra renders if TodoStats is also wrapped in React.memo

Sign up free to play

Answer all 10 questions (7 more), see explanations for every answer, and track your score.