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
- For
action.type === 'add', return[...todos, { id: action.id, title: action.title, done: false }]. - For
action.type === 'toggle', returntodos.map(...): for the matchingid— a new object{ ...todo, done: !todo.done }, otherwise thetodoitself. - In all other cases, return
todos. - No
push,splice, ortodo.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
CONSOLE · Console output
Hit Run to see the result...