State Management Patterns
Marking a Todo as Completed
When marking a todo item as completed, we directly toggle the completed property of the todo item.
for (let i = 0; i < todos.length; i++) {
if (todos[i].id === todoIdNumber) {
todos[i].completed = !todos[i].completed;
}
}
Let’s refactor this code to use the map function:
// Helper function to toggle the completed status of a todo item
const toggleTodo = (todos, todoId) =>
todos.map((todo) =>
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo,
);
The toggleTodo function takes the todos array and the todoId as arguments. It uses the map function to create a new array by iterating over the todos array. For each todo item, it checks if the id matches the todoId. If it matches, it creates a new todo object with the completed property toggled. If it does not match, it returns the original todo object. This way, we are creating a new array with the updated todo item without mutating the original array.
Notice that the non-matching todos are returned as-is. They are the same object references, not copies. This is called structural sharing. The new array contains a new object only for the item that changed. The other items are the same objects that were in the original array. We never mutate those objects, so there is no reason to copy them. Libraries like Redux and Immer use this same pattern for immutable data.
Now, we can use the toggleTodo function in our event handler:
// Event handler to toggle the completed status of a todo item
const handleClickOnTodoList = (event) => {
if (event.target.id.includes("todo-text")) {
const todoId = event.target.id.split("-").pop();
todos = toggleTodo(todos, Number(todoId));
renderTodos();
}
};
Refactoring the Navbar Functions
Let’s refactor the handleClickOnNavbar and renderTodoNavBar functions so that they match the other functions we have refactored.
// Helper function to update the class list of a navbar element
const updateClassList = (element, isActive) => {
const classes = [
"underline",
"underline-offset-4",
"decoration-rose-800",
"decoration-2",
];
if (isActive) {
element.classList.add(...classes);
} else {
element.classList.remove(...classes);
}
};
// Helper function to render the navbar anchor elements
const renderTodoNavBar = (href) => {
Array.from(todoNav.children).forEach((element) => {
updateClassList(element, element.href === href);
});
};
// Event handler to filter the todos based on the navbar selection
const handleClickOnNavbar = (event) => {
if (event.target.tagName === "A") {
const href = event.target.href;
filter = href.split("/").pop() || "all";
renderTodos();
renderTodoNavBar(href);
}
};
Here is what changed in the refactored code:
-
Arrow Functions: Both functions are converted to arrow functions. The rest of the codebase already uses this shorter syntax.
-
Helper Functions: The logic for updating the class list of a navbar element is extracted into a separate helper function
updateClassList. This function takes an element and a boolean flagisActiveto determine whether to add or remove classes. -
Defaulting to “all”: The assignment to
filteruses the logical OR (||) to default to “all” when splitting and popping result in an empty string. -
Array.from and forEach: Instead of using a loop,
Array.fromis used to converttodoNav.children(which is an HTMLCollection) to an array, allowing the use offorEachfor iteration.
Checkpoint: Commit your progress.
git add .
git commit -m "todos-12: Refactor toggle and navbar with higher-order functions"
git push