TODO LIST using React

ยท

10 min read

  1. Imports:

    • useEffect and useState are imported from React for managing state and side-effects.

    • './App.css' imports the stylesheet.

    • TodoProvider is imported from the context, which will be used to provide state and functions throughout the app.

    • TodoForm and TodoItem are imported from components for rendering the form and individual todo items.

  2. State Initialization:

    • The todos state is initialized as an empty array useState([]). This state will store the list of todos.
  3. Add Todo:

    • addTodo: This function adds a new todo to the todos array.

      • It uses setTodos to update the state.

      • prev refers to the previous state of the todos.

      • The new todo is inserted at the front of the array (you can also add it to the back).

      • The id is generated using Date.now() to create a unique identifier for each todo.

      • After adding the todo, we use the spread operator (...) to include all properties of the new todo.

    const addTodo = (todo) => {
      setTodos((prev) => [{ id: Date.now(), ...todo }, ...prev]);
    }
  1. Update Todo:

    • updateTodo: This function updates a todo by matching its id and replacing it with the new todo object.

      • It uses map to iterate over the todos array.

      • If the id matches, it updates the todo; otherwise, it keeps the old one.

    const updateTodo = (id, todo) => {
      setTodos((prev) => prev.map((prevTodo) => (prevTodo.id === id ? todo : prevTodo)));
    }
  1. Delete Todo:

    • deleteTodo: This function removes a todo from the list based on its id.

      • It uses filter to create a new array without the todo whose id matches.

      • If the id is different, the todo remains in the array.

    const deleteTodo = (id) => {
      setTodos((prev) => prev.filter((todo) => todo.id !== id));
    }
  1. Toggle Todo Completion:

    • toggleComplete: This function toggles the completed property of a todo.

      • It uses map to iterate over all todos, checking for the matching id.

      • If the id matches, it toggles the completed property.

    const toggleComplete = (id) => {
      setTodos((prev) => prev.map((prevTodo) => 
        prevTodo.id === id ? { ...prevTodo, completed: !prevTodo.completed } : prevTodo
      ));
    }
  1. Local Storage:

    • The useEffect hook is used to interact with the browser's local storage.

    • The first useEffect runs once when the component mounts, fetching any todos stored in local storage and setting the state (setTodos).

    • The second useEffect runs whenever todos changes, storing the current state in local storage as a stringified JSON object.

    useEffect(() => {
      const todos = JSON.parse(localStorage.getItem("todos"));
      if (todos && todos.length > 0) {
        setTodos(todos);
      }
    }, []);

    useEffect(() => {
      localStorage.setItem("todos", JSON.stringify(todos));
    }, [todos]);
  1. Provider:

    • The TodoProvider is used to wrap the app and provide the todos, addTodo, updateTodo, deleteTodo, and toggleComplete functions to all components inside.

    • The value prop destructures the functions and passes them down to child components.

    <TodoProvider value={{ todos, addTodo, updateTodo, deleteTodo, toggleComplete }}>
      {/* App components here */}
    </TodoProvider>
  1. Rendering Todos:

    • Inside the return statement, the todos array is mapped over to render each TodoItem.

    • Each TodoItem receives a todo prop, which contains the individual todo data.

    {todos.map((todo) => (
      <div key={todo.id} className="w-full">
        <TodoItem todo={todo} />
      </div>
    ))}

Complete Code:

import { useEffect, useState } from 'react';
import './App.css';
import { TodoProvider } from './context';
import { TodoForm, TodoItem } from './components';

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

  const addTodo = (todo) => {
    setTodos((prev) => [{ id: Date.now(), ...todo }, ...prev]);
  };

  const updateTodo = (id, todo) => {
    setTodos((prev) => prev.map((prevTodo) => (prevTodo.id === id ? todo : prevTodo)));
  };

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

  const toggleComplete = (id) => {
    setTodos((prev) =>
      prev.map((prevTodo) => (prevTodo.id === id ? { ...prevTodo, completed: !prevTodo.completed } : prevTodo))
    );
  };

  useEffect(() => {
    const todos = JSON.parse(localStorage.getItem('todos'));
    if (todos && todos.length > 0) {
      setTodos(todos);
    }
  }, []);

  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  return (
    <TodoProvider value={{ todos, addTodo, updateTodo, deleteTodo, toggleComplete }}>
      <div className="bg-[#172842] min-h-screen py-8">
        <div className="w-full max-w-2xl mx-auto shadow-md rounded-lg px-4 py-3 text-white">
          <h1 className="text-2xl font-bold text-center mb-8 mt-2">Manage Your Todos</h1>
          <div className="mb-4">
            <TodoForm />
          </div>
          <div className="flex flex-wrap gap-y-3">
            {todos.map((todo) => (
              <div key={todo.id} className="w-full">
                <TodoItem todo={todo} />
              </div>
            ))}
          </div>
        </div>
      </div>
    </TodoProvider>
  );
}

export default App;
  1. Imports:

    • useContext is imported from react to allow components to access the context.

    • createContext is imported from react to create a context for managing the todo list.

  2. TodoContext:

    • TodoContext is created using createContext with an initial value. This context will hold the state of the todos and provide various methods (like adding, updating, and deleting todos) to manipulate the todo list.

    • The default state includes a todos array with a sample todo item that has properties: id, todo (the task description), and completed (which tracks if the task is marked as completed).

Explanation of default state:

  • todos: Contains an array with todo objects. Each todo is an object with:

    • id: A unique identifier for each todo.

    • todo: The text or message of the todo item.

    • completed: A boolean flag (defaulted to false) to represent whether the todo is completed or not.

Methods:

  • addTodo(todo): Function to add a new todo.

  • updateTodo(id, todo): Function to update an existing todo based on its ID.

  • deleteTodo(id): Function to delete a todo based on its ID.

  • toggleComplete(id): Function to toggle the completed status of a todo.

  1. useTodo Hook:

    • useTodo: A custom hook that simplifies the process of consuming the TodoContext within any component. It calls useContext with TodoContext to return the context value, allowing components to access the todos and the methods for manipulating them.
  2. TodoProvider:

    • TodoProvider: This is the Provider component from TodoContext used to wrap the application (or a portion of it) that needs access to the todo context. It passes the context value (todos and methods) down to all components within the provider.

Complete Code:

import React from "react";
import { useContext } from "react";
import { createContext } from "react";

// There are no databases; we're storing data in local storage
// We need to create an ID for each todo entry.
export const TodoContext = createContext({
    // Every todo will be an object.
    // Creating an initial 'todos' array with a sample todo.
    todos: [
        {
            id: 1,
            todo: "Todo msg",   // Initial todo text
            completed: false,   // Default status is not completed
        }
    ],
    // Functions to manage the todos are placeholders here.
    // These are implemented in the parent component (e.g., App.jsx).
    addTodo: (todo) => {},
    updateTodo: (id, todo) => {},
    deleteTodo: (id) => {},
    toggleComplete: (id) => {}
});

// Custom hook to access the TodoContext
export const useTodo = () => {
    return useContext(TodoContext);
}

// TodoProvider is the Provider that will wrap the components needing access to the todo context.
export const TodoProvider = TodoContext.Provider;
  1. Imports:

    • The useState hook is imported from React to manage local state.

    • The useTodo custom hook is imported from the context file to access todo-related functions (update, delete, toggle) from the context.

  2. Component Structure:

    • TodoItem: This component represents a single todo item. It receives the todo object as a prop, which contains the todo's details like id, todo (task), and completed (status).
  3. State Management:

    • isTodoEditable: A state that tracks whether the todo item is in edit mode (true) or not (false).

    • todoMsg: A state that holds the current message of the todo. This is initialized with the value of todo.todo (the text in the todo item) and can be edited.

  4. Context Functions:

    • updateTodo, deleteTodo, and toggleComplete are functions obtained from the useTodo context. These functions allow updating, deleting, and toggling the completion status of todos.
  5. Edit Todo:

    • The editTodo function is triggered when the user clicks on the edit button (pencil icon). This function updates the todo with the new todoMsg (text entered by the user) and then sets the isTodoEditable state to false, making the todo non-editable.
  6. Toggle Completed:

    • toggleCompleted: This function is triggered when the user clicks the checkbox. It calls the toggleComplete function from the context, which toggles the completed status of the todo.
  7. Rendering:

    • The component renders a div containing:

      • A checkbox to mark the todo as completed or not.

      • A text input to display and edit the todo's message.

      • Two buttons:

        • Edit/Save Button: This toggles between edit mode and save mode. If the todo is completed, this button is disabled.

        • Delete Button: A button to delete the todo, which calls deleteTodo with the todo's id.

  8. Dynamic Styling:

    • The div and input fields have dynamic styles based on the completed status of the todo:

      • If the todo is completed, the background color changes to a light green (bg-[#c6e9a7]), and the input field has a line-through style.

      • If the todo is not completed, it uses a different background color (bg-[#ccbed7]).


Complete Code:

import React, { useState } from 'react';
import { useTodo } from '../context';

function TodoItem({ todo }) {
    console.log("Adding todo:", { id: Date.now(), todo: todo, completed: false });

    // State for tracking whether the todo is editable
    const [isTodoEditable, setIsTodoEditable] = useState(false);
    // State for holding the todo message
    const [todoMsg, setTodoMsg] = useState(todo.todo);

    const { updateTodo, deleteTodo, toggleComplete } = useTodo();

    const editTodo = () => {
        // Update the todo with the new message
        updateTodo(todo.id, { ...todo, todo: todoMsg });
        // Set the todo as not editable after saving
        setIsTodoEditable(false);
    };

    const toggleCompleted = () => {
        // Toggle the completion status of the todo
        console.log(todo.id);
        toggleComplete(todo.id);
    };

    return (
        <div
            className={`flex border border-black/10 rounded-lg px-3 py-1.5 gap-x-3 shadow-sm shadow-white/50 duration-300 text-black ${
                todo.completed ? "bg-[#c6e9a7]" : "bg-[#ccbed7]"
            }`}
        >
            <input
                type="checkbox"
                className="cursor-pointer"
                checked={todo.completed}
                onChange={toggleCompleted}
            />
            <input
                type="text"
                className={`border outline-none w-full bg-transparent rounded-lg ${
                    isTodoEditable ? "border-black/10 px-2" : "border-transparent"
                } ${todo.completed ? "line-through" : ""}`}
                value={todoMsg}
                onChange={(e) => setTodoMsg(e.target.value)}
                readOnly={!isTodoEditable}
            />
            {/* Edit, Save Button */}
            <button
                className="inline-flex w-8 h-8 rounded-lg text-sm border border-black/10 justify-center items-center bg-gray-50 hover:bg-gray-100 shrink-0 disabled:opacity-50"
                onClick={() => {
                    if (todo.completed) return;

                    if (isTodoEditable) {
                        editTodo();
                    } else setIsTodoEditable((prev) => !prev);
                }}
                disabled={todo.completed}
            >
                {isTodoEditable ? "๐Ÿ“" : "โœ๏ธ"}
            </button>
            {/* Delete Todo Button */}
            <button
                className="inline-flex w-8 h-8 rounded-lg text-sm border border-black/10 justify-center items-center bg-gray-50 hover:bg-gray-100 shrink-0"
                onClick={() => deleteTodo(todo.id)}
            >
                โŒ
            </button>
        </div>
    );
}

export default TodoItem;
  1. Imports:

    • The useState hook is imported from React to manage local state.

    • The useTodo custom hook is imported from the context file to access the addTodo function from the context.

  2. Component Structure:

    • TodoForm: This component is responsible for managing the form input, where users can add new todos.
  3. State Management:

    • todo: This state holds the value of the todo input field. Initially, it's an empty string.
  4. Context Functions:

    • addTodo: This function is obtained from the useTodo context, and it allows adding a new todo to the list.
  5. Add Todo:

    • The add function is triggered when the form is submitted. It first prevents the default form submission behavior using e.preventDefault().

    • If the todo input is empty, the function returns early, preventing the todo from being added.

    • Otherwise, it calls the addTodo function, passing an object with the todo text and the completed status set to false. The id is automatically set to Date.now() for a unique identifier.

    • After adding the todo, the input field is cleared by setting setTodo("").

  6. Rendering:

    • The component renders a form with:

      • An input field for entering the todo.

      • A submit button to add the todo to the list.

  7. Dynamic Styling:

    • The input and button elements are styled using Tailwind CSS classes to give a clean and responsive layout.

Complete Code:

import React, { useState } from 'react';
import { useTodo } from '../context';

function TodoForm() {
    // State to track the value of the todo input
    const [todo, setTodo] = useState("");
    // Accessing the addTodo function from the context
    const { addTodo } = useTodo();

    // Method to handle form submission
    const add = (e) => {
        e.preventDefault();
        // If todo is empty, do nothing
        if (!todo) return;
        // Adding the new todo to the list (with a generated id)
        addTodo({ todo, completed: false });
        // Clear the input field after adding the todo
        setTodo("");
    };

    return (
        <form onSubmit={add} className="flex">
            <input
                type="text"
                placeholder="Write Todo..."
                className="w-full border border-black/10 rounded-l-lg px-3 outline-none duration-150 bg-white/20 py-1.5"
                value={todo}
                // Update todo state as the user types
                onChange={(e) => setTodo(e.target.value)}
            />
            <button type="submit" className="rounded-r-lg px-3 py-1 bg-green-600 text-white shrink-0">
                Add
            </button>
        </form>
    );
}

export default TodoForm;
ย