We will refactor the code to incorporate a functional programming paradigm. In functional programming, our focus will be on:
Higher-Order Functions: These are functions that either accept other functions as arguments or return functions.
Closures: Functions that retain access to variables from their outer (enclosing) scope, even after the outer function has finished executing.
Immutable Data: Rather than altering data, we’ll aim to create new data structures.
Pure Functions: These are functions that consistently yield the same output for identical input and do not produce any side effects.
Step 1: Render Todos
Let’s focus on the renderTodos function. We are using two for loops to filter the todos based on the current filter setting and then render them to the DOM.
In JavaScript, arrays have built-in higher-order functions like filter, map, reduce, and forEach. We can use these functions to refactor the code and make it more “functional.” For instance, for the second loop where we iterate over the filteredTodos array, we can use the forEach function to loop through the filteredTodos array and render each todo item to the DOM. Here is an example of how you can use the forEach function to render the todos:
We can define a separate function to render each todo item and pass it as an argument to the forEach function.
Notice that the forEach function takes a function as an argument, which will be called for each element in the filteredTodos array. This is an example of a higher-order function where we pass a function as an argument to another function. In this case, the forEach function is the higher-order function, and the renderTodoItem function is the callback function.
The forEach function will call the renderTodoItem function (hence it is called the callback function) for each todo item in the filteredTodos array, passing the todo item as an argument. The renderTodoItem function will then render the todo item to the DOM.
We can define the renderTodoItem function right before where we are calling the forEach function, inside the renderTodos function. In JavaScript functions can be defined anywhere in the code, including inside other functions. Alternatively, we can define renderTodoItem separately at the top of the file. This alternative will be useful if we want to reuse the renderTodoItem function in other parts of the code.
In our case, we will not be reusing the renderTodoItem function, so defining it inside the renderTodos function is a good choice. In fact, we can define the renderTodoItem function right where we are calling the forEach function as shown below:
If you define the renderTodoItem function right where you are calling the forEach function, you would not be able to reuse it in other parts of the code. If this is not a concern, you can define it inline like this. Moreover, you can make it an anonymous function by removing the function name.
The anonymous function will still be called for each todo item in the filteredTodos array, but you won’t be able to reuse it in other parts of the code. It has a more concise syntax and is useful when you don’t need to reuse the function. On that note, you can also use arrow functions for an even more concise syntax.
Arrow functions are a more concise way to define functions in JavaScript. They have a shorter syntax and are typically used for anonymous functions that are passed as arguments to other functions.
Let’s complete the implementation of this arrow function to render each todo item to the DOM.
The body of the arrow function is the same as the code that was inside the second loop. We are creating a div element for each todo item, setting the text content, adding classes based on the completed property, and appending the elements to the DOM. Replace the second loop with this code snippet to render the todos using the forEach function.
We can further refactor the code by extracting the logic to create the todo text element and the todo edit input element into separate helper functions. This will make the code more modular and easier to read.
Now we can use these helper functions inside the forEach function to create the todo text and edit input elements and append them to the todo item.
Here is another way of performing the same operation using the map function.
The map function creates a new array with the results of calling a provided function on every element in the calling array. In this case, we are creating an array of todo elements by mapping each todo item to a todo element using the createTodoItem function. We then append all the todo elements to the todoListElement using the spread operator ....
Both of these approaches are valid and achieve the same result. The choice between using forEach and map depends on the specific requirements of your application. The map function is often preferred when you want to transform each element in an array and create a new array with the transformed elements. The forEach function is used when you want to perform an operation on each element in an array without creating a new array.
Step 2: Filter Todos
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, which is a higher-order function that creates a new array with all elements that pass the test implemented by the provided function.
For simplicity, let’s start by considering that our goal is to show only completed todos. We can use the filter function as follows:
Notice how the filter function takes a function as an argument that returns a boolean value. The filter function will call this function for each element in the todos array and create 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.
Given the todo.completed property is already a boolean value, we can further simplify the code by directly returning the todo.completed property.
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.
Now this is where we can claim the code is a more readable and concise way to filter the todos array.
A fundamental difference between filter and forEach is that filter creates a new array with the elements that pass the condition, while forEach simply iterates over the elements without creating a new array. This is an important distinction to keep in mind when using these functions.
In our case we need to consider the filter settings to show completed, active, or all todos. We can refactor the code to use the filter function with a conditional statement to filter the todos based on the current filter setting. We can define a helper function to filter the todos based on the current filter setting.
Pay attention to the use of the spread operator ... when returning all todos. This is to ensure that we are returning a new array with the same elements as the original todos array. This is an example of immutability, a key concept in functional programming. The filter function is a good example of this principle, as it creates a new array with the filtered elements without modifying the original 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.
This refactoring makes the code more modular and easier to read. The renderTodos function now uses the filterTodos function to get the filtered todos based on the current filter setting and then uses the map function to create an array of todo elements. Finally, it appends the todo elements to the todoListElement.
Since the return value of the filterTodos function is a new array containing the filtered todos, we can directly call forEach function on this array to render the todos to the DOM.
Alternatively, I can reduce this code to a single line by chaining the filterTodos and map function, and using replaceChildren functions together with the spread operator ... to replace the children of the todoListElement.
This is another common pattern in functional programming where you chain multiple functions together to transform data. There is also a tendency among functional programmers to write code in the form of a pipeline, where data flows through a series of functions, each transforming the data in some way. I personally prefer the first version of the code as it is more readable and easier to understand.
Step 3: Add a New Todo Item
Currently, we push a new todo item directly to the todos array when adding a new todo item.
As it was pointed out earlier, in functional programming, we aim to avoid mutating the original data and instead create new data structures. In this case, we are directly pushing a new todo item to the todos array, which mutates the original array. To adhere to the functional programming principle of immutability, we can create a new array with the existing todos and the new todo item.
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 that aim, we should change the todos array declaration from const to let.
Now, we can use the addTodo function to add a new todo item to the todos array without mutating the original array.
You might be wondering what is the point of this change. In this simple example, it might not seem necessary to avoid mutating the original array. However, in more complex applications, immutability can help prevent bugs and make the code easier to reason about. This patterns is also used in libraries like React to manage state updates. If your React component state is an object or an array, you should never mutate it directly. Instead, you should create a new object or array with the updated values.
Step 4: Mark a Todo Item as Completed
When marking a todo item as completed, we directly toggle the completed property of the todo item.
Let’s refactor this code to use the map function:
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 doesn’t 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.
Now, we can use the toggleTodo function to toggle the completed status of a todo item. In the refactoring process, we also create a few helper functions to make the code more readable and modular.
Step 5: Select a Filter
When selecting a filter, we directly update the filter variable.
Let’s refactor the code that toggles the completed status of a todo item. Currently, we are using a loop to find the todo item with the matching id and toggle its completed status.
In this code snippet, we are iterating over the todos array to find the todo item with the matching id and toggle its completed status. We can refactor this code to use the find function, which returns the first element in the array that satisfies the provided testing function.
In this refactored code, we are using the find function to find the todo item with the matching id and toggle its completed status. The find function returns the first element in the array that satisfies the provided testing function. If a todo item with the matching id is found, we toggle its completed status and then re-render the app UI.
Step 6: Refactor renderTodoNavBar and handleClickOnNavbar
Let’s refactor the handleClickOnNavbar and renderTodoNavBar functions to make them align with the other refactored functions.
Here are the key changes made in the refactored code:
Arrow Functions: Both functions are converted to arrow functions, making them consistent with the rest of the codebase that uses this more concise function 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 flag isActive to determine whether to add or remove classes.
Defaulting to “all”: The action assignment uses 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.from is used to convert todoNav.children (which is an HTMLCollection) to an array, allowing the use of forEach for iteration.
Step 7: Create the Todo App Factory Function
Let’s refactor the code to create a factory function that encapsulates the todo app logic. This function will return an object with methods to interact with the todo app.
In this factory function, we define the initial state of our app (todos, nextTodoId, and filter) and return an object with methods to interact with the todo app. The addTodo, toggleTodo, setFilter, and getTodos methods encapsulate the logic for adding a new todo item, toggling the completed status of a todo item, setting the filter, and getting the filtered todos, respectively.
We are using a functional programming techncique called closures to maintain the state of the todo app within the factory function. The todos, nextTodoId, and filter variables are accessible within the returned object’s methods due to the closure created by the factory function. However, these variables are not directly accessible outside the factory function, ensuring data encapsulation.
Notice the helper functions addTodo, toggleTodo, and filterTodos were already defined in earlier steps. I have updated the addTodo function to include the newTodoId parameter, which is used to assign a unique id to each new todo item.
You should now use the factory function to create a todo app instance and interact with it using the defined methods.
You can now use the todoApp object to interact with the todo app. For example, to add a new todo item, you can call the addTodo method.
Step 8: Putting It All Together
Here is the final refactored code that incorporates all the changes we made in the previous steps.
Let’s summarize the changes made in this refactoring process and reflect on how they align with the principles of functional programming:
Higher-Order Functions: We used higher-order functions like map, filter, and forEach to refactor the code and make it more functional. These functions take other functions as arguments or return functions, allowing us to write more concise and expressive code.
Closures: We used closures to maintain the state of the todo app within the factory function. The todos, nextTodoId, and filter variables are accessible within the returned object’s methods due to the closure created by the factory function.
Immutable Data: We avoided mutating the original data and instead created new data structures. For example, when adding a new todo item or toggling the completed status of a todo item, we created new arrays with the updated values without modifying the original arrays.
Pure Functions: We aimed to write pure functions that always produce the same output for the same input and have no side effects. The helper functions addTodo, toggleTodo, and filterTodos are pure functions that take input and return output without modifying external state.
By following these principles and refactoring the code to adopt a more functional programming paradigm, we have made the code more modular, readable, and maintainable. We have also learned how to use higher-order functions, closures, and immutable data to write cleaner and more expressive code.
Step 9: Mark All Todos as Completed
Let’s add a feature to mark all todos as completed. There is a button with the id mark-all-completed that we can use to trigger this action.
Before implementing the handleMarkAllCompleted function, let’s define a helper function to toggle the status of all todos to completed.
We are using the map function to create a new array with all todos marked as completed. The markAllTodosCompleted function takes the todos array as input and returns a new array with all todos marked as completed.
Let’s update the factory function to include a method to mark all todos as completed.
Now, we can implement the handleMarkAllCompleted function to mark all todos as completed.
Run the code and test the “Mark All Completed” feature by clicking the “Mark All Completed” button. You should see all todos marked as completed.
Step 10: Clear Completed Todos
Let’s add a feature to clear all completed todos. There is a button with the id clear-completed that we can use to trigger this action.
Before implementing the clearCompletedTodos function, let’s define a helper function to filter out completed todos.
We are using the filter function to create a new array with all completed todos filtered out. The deleteCompletedTodos function takes the todos array as input and returns a new array with all completed todos removed.
Let’s update the factory function to include a method to delete all completed todos.
Now, we can implement the clearCompletedTodos function to delete all completed todos.
Run the code and test the “Clear Completed” feature by clicking the “Clear Completed” button. You should see all completed todos removed from the list.
Step 11: Display the Number of Active Todos
There is a span element with the id todo-count that we can use to display the number of active todos. Let’s get hold of this element and update it with the count of active todos.
Next, let’s update the factory function to include a method to get the number of active todos.
Here we are using the reduce function to calculate the number of active todos. The reduce function takes an accumulator acc and the current todo item as arguments. For each todo item, it increments the accumulator by 1 if the todo is not completed. The initial value of the accumulator is set to 0.
Let’s update the renderTodos function to display the number of active todos.
Here we are updating the innerText of the activeTodosCount element to display the number of active todos. We are using a template literal to display the count and handle the pluralization of the word “item” based on the count.
Run the code and test the display of the number of active todos. You should see the count of active todos displayed on the screen.
Step 12: The Final Code
Here is the final refactored code that incorporates all the changes we made in the previous steps.
This final code includes all the features we implemented in the previous steps, such as adding a new todo item, toggling the completed status of a todo item, filtering todos based on the current filter, marking all todos as completed, clearing completed todos, and displaying the number of active todos. The code is modular, readable, and follows the principles of functional programming.