Jamie Balfour

Welcome to my personal website.

Find out more about me, my personal projects, reviews, courses and much more here.

Part 7.3Typed reducers, context and generic components

Reducer actions

A discriminated union connects each action type with exactly the data it requires. Narrowing the type property makes the correct fields available in each reducer branch.

TypeScript
type TaskAction =
  | { type: "added"; task: Task }
  | { type: "toggled"; id: string }
  | { type: "deleted"; id: string };

function tasksReducer(
  tasks: Task[],
  action: TaskAction
): Task[] {
  switch (action.type) {
    case "added":
      return [...tasks, action.task];
    case "deleted":
      return tasks.filter(
        (task) => task.id !== action.id
      );
  }
}
    

Safe context

Create context with undefined as its default, then expose a custom Hook that throws when used outside the provider. Consumers receive the real value without repeatedly checking for absence.

Generic components

A generic component can preserve the item type while rendering different lists. The caller supplies data, a stable key function and a renderer; TypeScript keeps all three connected.

TSX
type ListProps<T> = {
  items: T[];
  getKey: (item: T) => React.Key;
  renderItem: (item: T) => React.ReactNode;
};

function List<T>({
  items,
  getKey,
  renderItem
}: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={getKey(item)}>
          {renderItem(item)}
        </li>
      ))}
    </ul>
  );
}
    
  1. Add a renamed action to the reducer union and implementation.
  2. Create typed task-data and dispatch contexts.
  3. Make custom Hooks throw outside their providers.
  4. Render the task array using the generic List component.

Extension challenge: make the reducer's default branch perform an exhaustive never check so an unhandled action becomes a compiler error.

Feedback 👍
Comments are sent via email to me.