Pure calculations are easier to reuse
A pure function returns a result based on its arguments without changing outside state or performing effects such as printing, fetching, or writing storage. Effects are necessary in an app; give them a clear outer boundary instead of hiding them in calculations.
Copy only the parts you change. map creates a new array; spread creates a changed record. Unchanged records can remain shared if your program treats them as values. This convention is not enforced by const, and a shallow copy cannot protect nested objects you later mutate.
This small practice record uses a saved boolean. The final reading queue instead keeps a separate array of saved books; these are alternative representations, not two fields you must keep synchronized. The update below identifies a book by id and toggles its saved flag. For an unknown id, the returned array has the same values. We are choosing that policy instead of throwing. Whether the unchanged array is the same reference is a separate contract; do not over-specify identity unless the caller needs it.
A test should check both the new result and the original input. Testing only the result could let a mutating implementation pass. Later we will keep model updates pure and let a controller decide when to render.
Predict and trace
Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.
function toggle(books, id) {
return books.map(book =>
book.id === id ? { ...book, saved: !book.saved } : book
);
}
const before = [
{ id: "js", saved: false },
{ id: "web", saved: false }
];
const after = toggle(before, "js");
console.log(before[0].saved);
console.log(after[0].saved);
console.log(after[1] === before[1]);Check your prediction
false
true
trueThe unchanged second record may remain shared. The changed record must be independent for the original flag to stay false.
Preserve unchanged records intentionally
An immutable update usually needs a new container and a new record only where data changed. Reusing unchanged records is safe when the contract treats them as immutable. Cloning every record is not automatically more correct, and mutating a reused record breaks both old and new views.
Test values and identity separately. Value checks prove the requested change happened. Identity checks prove whether the old snapshot and unaffected records retain the relationship your caller expects.
const before = [
{ id: "a", saved: false },
{ id: "b", saved: false }
];
const after = before.map(book =>
book.id === "a" ? { ...book, saved: true } : book
);
console.log(before[0].saved, after[0].saved);
console.log(before === after, before[1] === after[1]);Check your prediction
false true
false true The changed record and outer array are new; the unaffected record is shared. This contract supports efficient comparisons while keeping the earlier saved flag unchanged.
const before = [{ id: "a", detail: { pages: 10 } }];
const after = before.map(book => ({ ...book }));
after[0].detail.pages = 20;
console.log(before[0].detail.pages);Check your prediction
20 Copying each outer book did not clone detail. A nested update needs a copy at that level too. The input being an array does not change the ordinary reference rules for its contents.
Try the rule on new inputs
Test a matching id, a missing id and an empty array. Decide whether a missing-id operation must return the exact original array or merely equivalent values, then write assertions matching that stated rule.
Build it yourself
Pure Functions & Updates: practise
Write rename(books, id, title), returning a revised array with only the matching title changed. Inputs are already validated. Print original and revised titles.
const books = [{ id: "js", title: "Old" }];Hint 1: the idea
Map preserves membership and lets you replace exactly one record.
Hint 2: a first step
Match the id and spread before assigning the new title.
Show one solution
function rename(books, id, title) {
return books.map(book => (book.id === id ? { ...book, title } : book));
}
const books = [{ id: "js", title: "Old" }];
const revised = rename(books, "js", "New");
console.log(books[0].title);
console.log(revised[0].title);Old
NewTest an unknown id and empty input. Then rename twice using the first result as the second input; confirm the first snapshot still says New.
Test a changed requirement
Test an unknown id and empty input. Then rename twice using the first result as the second input; confirm the first snapshot still says New.
Explain before moving on
Which changes count as effects? What is the shallow-copy limitation here?
Completion is saved in this browser only. It does not measure mastery.