Building a real data model
This project combines arrays of objects from lesson 5-1 with the toolkit from unit 7, and one method does most of the work: todos.find(t => t.id === id) locates the single todo whose id matches and hands back the object itself, ready to modify.
The alternatives all fall short here. filter finds the right todo but wraps it in an array, so every use would need an extra [0]. map returns a same-length array of booleans rather than the matching item. And includes compares whole values rather than looking at a property, so it cannot search by id at all.
The data model behind every to-do app
Strip the buttons off any to-do app and what remains is a data model: an array of objects plus a few functions that change it. That is what this lesson builds, and it needs nothing but the console to be complete.
The design:
- Each todo is an object:
{ id: 1, title: "Learn JavaScript", done: false } - The
idis a number that never repeats, so any single todo can be pointed at later. addTodo(title)pushes a new object withpushfrom lesson 4-2 and bumps the id counter.completeTodo(id)usesfindfrom lesson 7-2 to locate the todo and flips itsdonetotrue.pendingCount()usesfilterfrom lesson 7-1 to count what is left, where!t.donereads as not done, using the!operator from lesson 3-1.
Data plus the functions that manage it is what engineers call the application's state, meaning the data a program remembers while it runs. Real apps are organized exactly this way, because the screen never invents anything of its own. It redraws whatever the state currently says.
That separation is why the entire model can be built and reasoned about with plain values and functions, before any interface exists. The same array-of-objects plus functions design sits behind the React and Node courses that follow.
The three functions that make up the model
Each function here is one line of real logic wrapped in a name, and each one comes from a lesson you have already worked through: push from 4-2, find from 7-2, filter from 7-1, and template literals from 1-3.
const todos = []; let nextId = 1; function addTodo(title) { todos.push({ id: nextId, title: title, done: false }); nextId = nextId + 1; } function completeTodo(id) { const todo = todos.find(t => t.id === id); if (todo) { todo.done = true; } } function pendingCount() { return todos.filter(t => !t.done).length; } addTodo("Learn JavaScript"); addTodo("Build a project"); addTodo("Take a walk"); completeTodo(1); console.log(pendingCount()); for (const t of todos) { console.log(`${t.id}. ${t.title} (done: ${t.done})`); }
Output
2 1. Learn JavaScript (done: true) 2. Build a project (done: false) 3. Take a walk (done: false)
Three details carry most of the design. nextId is declared with let because it is reassigned on every add, and it is what guarantees ids never repeat. The if (todo) guard inside completeTodo handles the case where no todo matched, since find returns undefined and setting .done on that would throw.
And completeTodo changes the object in place, which works because find returned a reference to the object living inside the array, not a copy of it. That is lesson 5-4's rule doing useful work rather than causing a bug.
Extending the model with a derived list
pendingTitles adds a new capability without touching any of the existing functions, which is the practical benefit of keeping data and behavior separate. It reads the state and returns something new.
const todos = [ { id: 1, title: "Learn JavaScript", done: true }, { id: 2, title: "Build a project", done: false }, { id: 3, title: "Take a walk", done: false } ]; function pendingTitles() { return todos.filter(t => !t.done).map(t => t.title); } console.log(pendingTitles());
Output
[ 'Build a project', 'Take a walk' ]
The body chains the two methods in the required order. todos.filter(t => !t.done) selects while whole objects are still available to test, and .map(t => t.title) then narrows each survivor to its title string.
A function like this is called a derived value, because it stores nothing of its own and computes its answer from the state every time it is called. That means it can never fall out of sync with todos, which is a guarantee a separate pendingTitles array would not give you.
Why each todo is an object
Storing each todo as { id, title, done } rather than as a bare title string is what lets a todo carry facts beyond its own text.
A string can only ever be its text. The moment a second fact is needed, such as whether the item is finished or which item this is, labeled slots become necessary, and that is exactly what objects from lesson 5-1 provide.
The payoff shows up when requirements grow. Adding a due date is one more property, { id, title, done, dueDate }, and existing functions keep working untouched because they only read the properties they care about. With bare strings, the same change would mean parallel arrays kept in lockstep by hand, or text parsing, and either one breaks the first time the data goes out of order.