Building a Todo App with Custom Hooks and State Management

In this module, we will build a Todo App using React and demonstrate the power of custom hooks and state management. The application will involve tasks like adding, deleting, and updating todos, while implementing efficient state management strategies and custom hooks to encapsulate logic.


Table of Contents

  1. Overview of the Todo App
  2. Setting Up the Project
  3. Creating Custom Hooks for Managing State
  4. State Management with useState and useReducer
  5. Handling User Input and Adding Todos
  6. Rendering Todos and Updating State
  7. Implementing Delete and Toggle Functionality
  8. Custom Hook for Todo Management
  9. Styling the Todo App
  10. Conclusion

1. Overview of the Todo App

In this module, we’ll create a simple Todo App that allows users to:

  • Add new todos
  • Toggle completion status of todos
  • Delete todos
  • Display a list of todos

We’ll utilize React hooks for managing state and encapsulate logic within custom hooks. The app will also demonstrate how to efficiently manage state in React using useState and useReducer.


2. Setting Up the Project

Before we begin, let’s set up the React project. You can use Create React App (CRA) or Vite for setting up the boilerplate for your project.

To set up with Vite, run the following commands:

bashCopyEdit# Create a new Vite project with React template
npm create vite@latest todo-app --template react

# Navigate to the project folder
cd todo-app

# Install dependencies
npm install

# Start the development server
npm run dev

For Create React App, run the following:

bashCopyEditnpx create-react-app todo-app
cd todo-app
npm start

This sets up the basic React project structure and serves the app on http://localhost:3000.


3. Creating Custom Hooks for Managing State

We’ll begin by creating a custom hook to manage the todos. A custom hook allows you to encapsulate and reuse logic.

Custom Hook: useTodos

javascriptCopyEditimport { useState } from 'react';

function useTodos() {
  const [todos, setTodos] = useState([]);

  const addTodo = (text) => {
    const newTodo = {
      id: Date.now(),
      text: text,
      completed: false,
    };
    setTodos((prevTodos) => [...prevTodos, newTodo]);
  };

  const toggleTodo = (id) => {
    setTodos((prevTodos) =>
      prevTodos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const deleteTodo = (id) => {
    setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
  };

  return { todos, addTodo, toggleTodo, deleteTodo };
}

export default useTodos;

The useTodos hook manages the following states:

  • todos: An array of todo items.
  • addTodo: A function to add a new todo.
  • toggleTodo: A function to toggle the completion status of a todo.
  • deleteTodo: A function to delete a todo by its ID.

4. State Management with useState and useReducer

While useState is great for simple state management, we can also use useReducer when dealing with complex state logic. For the sake of simplicity, we’ll stick with useState in this module, but we’ll mention useReducer as an alternative.

When to Use useReducer:

  • When the state logic is more complex.
  • When state updates are dependent on the previous state.
  • When you want to separate logic from the UI.

Here’s how you might implement useReducer for this Todo app:

javascriptCopyEditimport { useReducer } from 'react';

const initialState = [];

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.payload];
    case 'TOGGLE_TODO':
      return state.map((todo) =>
        todo.id === action.payload
          ? { ...todo, completed: !todo.completed }
          : todo
      );
    case 'DELETE_TODO':
      return state.filter((todo) => todo.id !== action.payload);
    default:
      return state;
  }
}

function useTodos() {
  const [todos, dispatch] = useReducer(todoReducer, initialState);

  const addTodo = (text) => {
    dispatch({ type: 'ADD_TODO', payload: { id: Date.now(), text, completed: false } });
  };

  const toggleTodo = (id) => {
    dispatch({ type: 'TOGGLE_TODO', payload: id });
  };

  const deleteTodo = (id) => {
    dispatch({ type: 'DELETE_TODO', payload: id });
  };

  return { todos, addTodo, toggleTodo, deleteTodo };
}

export default useTodos;

5. Handling User Input and Adding Todos

In the App.js file, you’ll create an input field where users can type in the name of a new todo. When the user submits the form, it triggers the addTodo function.

javascriptCopyEditimport React, { useState } from 'react';
import useTodos from './useTodos';

function TodoApp() {
  const { todos, addTodo } = useTodos();
  const [todoText, setTodoText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (todoText) {
      addTodo(todoText);
      setTodoText('');
    }
  };

  return (
    <div>
      <h1>Todo App</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={todoText}
          onChange={(e) => setTodoText(e.target.value)}
          placeholder="Enter a new todo"
        />
        <button type="submit">Add Todo</button>
      </form>
      <TodoList todos={todos} />
    </div>
  );
}

6. Rendering Todos and Updating State

To display the todos, create a TodoList component that maps over the todos array and renders each todo.

javascriptCopyEditfunction TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <TodoItem todo={todo} />
        </li>
      ))}
    </ul>
  );
}

7. Implementing Delete and Toggle Functionality

Now, let’s add the functionality to toggle the completion status and delete a todo. We’ll create a TodoItem component that receives a todo as a prop and adds buttons for toggling and deleting.

javascriptCopyEditfunction TodoItem({ todo }) {
  const { toggleTodo, deleteTodo } = useTodos();

  return (
    <div>
      <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
        {todo.text}
      </span>
      <button onClick={() => toggleTodo(todo.id)}>Toggle</button>
      <button onClick={() => deleteTodo(todo.id)}>Delete</button>
    </div>
  );
}

8. Custom Hook for Todo Management

We already created the useTodos hook, which encapsulates the logic for adding, toggling, and deleting todos. This custom hook makes the logic reusable and cleaner.


9. Styling the Todo App

To style the app, we can use any styling method we prefer (CSS, SCSS, styled-components, etc.). For simplicity, let’s apply some basic styles to make the app look clean.

cssCopyEdit/* styles.css */
body {
  font-family: Arial, sans-serif;
  background-color: #f4f4f4;
  margin: 0;
  padding: 0;
}

h1 {
  text-align: center;
  margin-top: 20px;
}

form {
  display: flex;
  justify-content: center;
  margin-bottom: 20px;
}

input {
  padding: 8px;
  font-size: 16px;
  margin-right: 10px;
}

button {
  padding: 8px;
  font-size: 16px;
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  display: flex;
  justify-content: space-between;
  padding: 10px;
  margin: 10px;
  background-color: white;
  border-radius: 4px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

10. Conclusion

In this module, we’ve built a functional Todo app in React using custom hooks for state management and encapsulating logic. We also explored the power of useState and useReducer for managing state in React. Additionally, we learned how to handle user input, dynamically update the state, and render the todo items with the ability to toggle and delete them.

This hands-on approach helps solidify your understanding of React concepts like hooks, state management, and component composition. You can extend this app further by adding features like local storage integration, filtering todos, and more.