BACK
Lessons45. scan and an immutable todo list
Lessons · 45

45. scan and an immutable todo list

Immutable updates — the rule of preserving the old

In UI apps, the convention is to update state immutably. That means: don't change old objects and arrays — create new ones. The old object must stay untouched.

Why it's needed

  • Comparison by reference: Angular OnPush, distinctUntilChanged, React, Signals — they all check "is this new?" via ===. A mutation won't change the reference → the UI won't update.
  • Time travel: immutable snapshots can be saved and rolled back.
  • Transparent debugging: you can predictably see what changed.

Immutable techniques

// add to an array:
[...old, newItem]

// update an array item conditionally:
old.map(item => item.id === id ? { ...item, done: !item.done } : item)

// update an object field:
{ ...old, name: 'new' }

Your task

  1. For action.type === 'add', return [...todos, { id: action.id, title: action.title, done: false }].
  2. For action.type === 'toggle', return todos.map(...): for the matching id — a new object { ...todo, done: !todo.done }, otherwise the todo itself.
  3. In all other cases, return todos.
  4. No push, splice, or todo.done = ... — those are mutations!
Solution spoiler · click to reveal
const { from, scan } = Rx;

const actions$ = from([
  { type: 'add', id: 1, title: 'learn RxJS' },
  { type: 'add', id: 2, title: 'build app' },
  { type: 'toggle', id: 1 },
]);

function formatTodos(todos) {
  return todos
    .map(todo => todo.title + '[' + (todo.done ? 'x' : ' ') + ']')
    .join(' | ');
}

const todos$ = actions$.pipe(
  scan((todos, action) => {
    if (action.type === 'add') {
      return [
        ...todos,
        { id: action.id, title: action.title, done: false },
      ];
    }

    if (action.type === 'toggle') {
      return todos.map(todo => {
        if (todo.id !== action.id) {
          return todo;
        }

        return { ...todo, done: !todo.done };
      });
    }

    return todos;
  }, [])
);

todos$.subscribe(todos => console.log('Todos: ' + formatTodos(todos)));
script.ts // TypeScript
CONSOLE · Console output
Hit Run to see the result...