Filtering and Immutability

Filtering Todos with filter

As part of rendering the todos, we are filtering the todos based on the current filter setting. We can refactor the code to use the filter function. It is a higher-order function: you give it a function, and it returns a new array with the elements for which that function returns true.

For simplicity, let’s start by considering that our goal is to show only completed todos. We can use the filter function as follows:

const completedTodos = todos.filter((todo) => {
  if (todo.completed) {
    return true;
  } else {
    return false;
  }
});

Notice how the filter function takes a function as an argument that returns a boolean value. The filter function calls this function for each element in the todos array and creates a new array containing only the elements for which the function returns true. In this case, we are filtering the todos array to create a new array containing only the completed todos.

We can simplify the code by using a ternary operator to return true or false based on the completed property of the todo.

const completedTodos = todos.filter((todo) => {
  return todo.completed ? true : false;
});

Given the todo.completed property is already a boolean value, we can further simplify the code by directly returning the todo.completed property.

const completedTodos = todos.filter((todo) => {
  return todo.completed;
});

In arrow functions, if the function body consists of a single expression, you can omit the curly braces {} and the return keyword. The expression will be implicitly returned. You can also eliminate the parentheses around the parameter if there is only one parameter.

const completedTodos = todos.filter((todo) => todo.completed);

One thing to note about filter is that it does not modify the original todos array. It creates and returns a new array containing only the elements that pass the condition, and the original array stays as it was. That is different from forEach, which iterates over the elements without producing a new array, and from methods like push or splice, which modify the array in place.

Creating new data instead of modifying existing data is called immutability. It is a key principle in functional programming. When you do not mutate data, your code is easier to reason about, because you can rely on the original array not being changed by some other function.

In our case, we need to consider the filter settings to show completed, active, or all todos. Let’s define a helper function to filter the todos based on the current filter setting.

// Helper function to filter todos based on the current filter setting
const filterTodos = (todos, filter) => {
  if (filter === "active") {
    return todos.filter((todo) => !todo.completed);
  } else if (filter === "completed") {
    return todos.filter((todo) => todo.completed);
  } else {
    return [...todos];
  }
};

Notice the [...todos] in the else branch. When the filter is "all", we do not need to filter anything, but we still return a new array (using the spread operator) rather than returning the original todos array directly. This keeps the function consistent: whichever branch runs, the caller gets a new array, so the caller cannot accidentally modify the original todos array.

With this function in place, we can refactor the renderTodos function to use the filterTodos function to get the filtered todos and then render them to the DOM.

// Function to render the todos based on the current filter
const renderTodos = () => {
  todoListElement.innerHTML = "";  // Clear the current list to avoid duplicates

  const filteredTodos = filterTodos(todos, filter);
  const todoElements = filteredTodos.map(createTodoItem);
  todoListElement.append(...todoElements);
};

We can reduce this code to a single line by chaining the filterTodos and map functions, and using replaceChildren together with the spread operator ... to replace the children of the todoListElement.

// Function to render the todos based on the current filter
const renderTodos = () => {
  todoListElement.replaceChildren(
    ...filterTodos(todos, filter).map(createTodoItem),
  );
};

Chaining functions together to transform data is another common pattern in functional programming. Functional programmers often write code as a pipeline, where the data is passed through a series of functions, and each one transforms it in some way. I personally prefer the first version of the code, because it is easier to read.

Adding a New Todo Item

Currently, we push a new todo item directly to the todos array when adding a new todo item.

todos.push({ id: nextTodoId++, text: todoText, completed: false });

Notice that push modifies the todos array in place, which means it mutates it. We said above that filterTodos always returns a new array and leaves the original alone. Let’s do the same thing here and create a new array instead of mutating the existing one.

// Helper function to create a new array with the existing todos and a new todo item
const addTodo = (todos, newTodoText) => [
  ...todos,
  { id: nextTodoId++, text: newTodoText, completed: false },
];

The addTodo function creates a new array by spreading the existing todos array and adding the new todo item to the end. In our application, we should use the return value of this function to replace the todos array. To do that, we need to change the todos array declaration from const to let.

- const todos = [
+ let todos = [
    { id: 1, text: "Buy milk", completed: false },
    { id: 2, text: "Buy bread", completed: false },
    { id: 3, text: "Buy jam", completed: true },
  ];

Now, we can use the addTodo function to add a new todo item to the todos array without mutating the original array.

// Event handler to create a new todo item
const handleKeyDownToCreateNewTodo = (event) => {
  if (event.key === "Enter") {
    const todoText = event.target.value.trim();
    if (todoText) {
      todos = addTodo(todos, todoText);
      event.target.value = "";  // Clear the input
      renderTodos();
    }
  }
};

We now follow the same pattern when we filter and when we add, so every operation on our data produces a new array. You will see this pattern often in libraries like React, where component state should never be mutated directly.

Checkpoint: Commit your progress.

git add .
git commit -m "todos-11: Implement filtering with immutability"
git push