SkillByAIOpen interactive version →

Lesson 3 / 25

Unidirectional Data Flow and Immutability

Actions in, new state out, UI re-renders.

One direction, new objects

Redux formalises a unidirectional data flow: the UI dispatches an action (a plain object with a type and usually a payload), a reducer computes the next state from the current state and the action, the store saves it and notifies subscribers, and components re-render with the new values. Reducers must be pure and must not mutate their inputs; they return new objects. Immutability lets libraries detect changes with a cheap reference comparison (prev !== next) instead of a deep comparison. Zustand follows the same idea: set merges a new state object, and selectors compare by reference.

An immutable update by hand

What Redux Toolkit will later write for you.

type Todo = { id: string; text: string; done: boolean };
type State = { todos: Todo[] };

function todosReducer(state: State, action: { type: string; payload: string }): State {
  switch (action.type) {
    case 'todos/toggled':
      return {
        ...state,
        todos: state.todos.map((t) =>
          t.id === action.payload ? { ...t, done: !t.done } : t
        ),
      };
    default:
      return state; // unknown action: return the same reference
  }
}

Never mutate outside Immer

state.todos.push(x) is a bug in a hand-written reducer or in a Zustand set callback without the immer middleware: the reference does not change, so subscribers may not update.

Quick check: Why do Redux and Zustand rely on immutable updates?

  • So the server can validate the state
  • Because JavaScript objects cannot be mutated
  • To make state smaller in memory
  • So changes can be detected with a cheap reference comparison
Answer

So changes can be detected with a cheap reference comparison — A new reference signals that something changed.