TO-DO APP USING REACT, VITE, AND BOOTSTRAP - PART 2: CREATE CUSTOM COMPONENTS

In this post, we will continue to develop our application by creating custom components. Also, we'll persist data using localStorage.

ALL PARTS:

  1. To-Do App Using React, Vite, And Bootstrap: Set Up the Application
  2. To-Do App Using React, Vite, And Bootstrap: Create Custom Components
  3. To-Do App Using React, Vite, And Bootstrap: Create Tasks
  4. To-Do App Using React, Vite, And Bootstrap: Edit Tasks
  5. To-Do App Using React, Vite, And Bootstrap: Delete and Update Tasks Status

Create a Custom Component

In this step, we'll extract the task list rendering logic from the App component and encapsulate it within its custom component called Tasks.

Let's begin by creating a new file named Tasks.jsx in the src folder. Inside this file, we'll define the Tasks component:

import { v4 as uuidv4 } from "uuid";

const Tasks = () => {
  const tasks = [
    { id: uuidv4(), title: "Feed the cats", completed: false },
    { id: uuidv4(), title: "Exercise", completed: true },
    { id: uuidv4(), title: "Go Shopping", completed: false },
  ];
  return (
    <>
      <h1>Tasks</h1>
      <ul className="mt-5 list-unstyled">
        {tasks.map((task) => (
          <li key={task.id}>
            <span>{task.title}</span>
          </li>
        ))}
      </ul>
    </>
  );
};

export default Tasks;

We've put the heading and the list element inside a React fragment. React fragments let you group elements without adding a new element to the DOM.

Next, let's import the Tasks component in the App component. After the other import statements, add this:

import Tasks from "./Tasks.jsx";

We can delete the tasks variable in this component. Also we can delete the uuid import statement. And we can delete the content within the container div in the App component and put the Tasks component instead:

<div className="container">
  <Tasks />
</div>

Introduction to Hooks: The state Hook

React Hooks are functions that allow functional components to "hook into" React features like state and lifecycle methods. They provide a way to use state and other React features without writing a class component.

Understanding State:

In React, the state represents the data that a component manages internally. It allows components to store and manage information that can change over time, such as user input, fetched data, or UI state.

Introducing the useState Hook:

The useState hook is one of the most commonly used React hooks. It allows functional components to add stateful behavior by declaring state variables. Here's how it works:

  • Syntax: const [state, setState] = useState(initialState);
  • Parameters:
    • initialState: The initial value of the state variable.
  • Return Value:
    • state: The current value of the state variable.
    • setState: A function that allows updating the state variable.

We have a list of tasks that we've hard-coded into our application. However, we want it to be updated when we add, edit, or delete tasks. 

That's why we'll use the useState hook. First, let's import it into the Tasks component:

import { useState } from "react";

You can delete this import from the App component.

Now, let's remove the regular variable tasks, and add a state variable:

const [tasks, setTasks] = useState([
  { id: uuidv4(), title: "Feed the cats", completed: false },
  { id: uuidv4(), title: "Exercise", completed: true },
  { id: uuidv4(), title: "Go Shopping", completed: false },
]);

PERSIST DATA

To persist data during a session in a React application, we can use a browser storage mechanism such as localStorage. localStorage is a browser API that allows us to store key-value pairs locally in the user's web browser. The data stored in localStorage persists even after the browser is closed and reopened, making it suitable for storing session data, user preferences, or any other data that needs to persist across page reloads within the same browser.

You can retrieve data from localStorage using the getItem method. It takes one argument: the key of the item you want to retrieve. We can use this method to retrieve data into our application. 

Initializing State with localStorage Data

In the Tasks component, we're initializing the tasks state using a callback function provided to useState.

This callback function retrieves the tasks data from localStorage using getItem("tasks").  

Let's modify the tasks variable like this:

const [tasks, setTasks] = useState(() => {
  const savedTasks = localStorage.getItem("tasks");
  return savedTasks ? JSON.parse(savedTasks) : [];
});

There are no tasks for no, so nothing is shown.

Updating localStorage When tasks State Changes

We can use the useEffect hook to listen for changes to the task's state.
Let's first import it into the Tasks component:

import { useState, useEffect } from "react";

Then we add this before the return statement:

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

We're using the useEffect hook to listen for changes to the tasks state. 
Whenever the tasks state changes, we're updating the data in localStorage using setItem("tasks", JSON.stringify(tasks)).

We're converting the tasks array to a JSON string using JSON.stringify because localStorage can only store strings.

With this setup, the tasks data is saved to localStorage whenever it changes, and it's retrieved from localStorage when the App component mounts. This ensures that the tasks data persists across page reloads within the same browser session, providing a seamless user experience.

We can delete the uuid import statement now, because we don't need it anymore.

Create a Task Component

One of the fundamental principles of React development is the concept of modularity – breaking down the user interface into smaller, reusable components. This approach not only enhances code organization but also promotes code reusability and maintainability.

To adhere to this principle, we'll add a separate Task component. This component is responsible for rendering individual task items within our application. By isolating the rendering logic for a single task into its own component, we achieve a clear separation of concerns.

 Let's create a file called Task.jsx in the src folder.  

Inside this component, we will return the list item currently in the Tasks component. Let's edit it as follows:

const Task = ({ task }) => {
  return (
    <li className="d-flex align-items-center border-bottom border-dark pb-2 pt-2">
      <span>{task.title}</span>
    </li>
  );
};

export default Task;

We've added some Bootstrap classes to this component. Now, let's import it into the Tasks component:

import Task from "./Task.jsx";

And we'll use it inside the tasks list. We'll edit it like this:

<ul className="mt-5 list-unstyled">
  {tasks.map((task) => (
    <Task key={task.id} task={task} />
  ))}
</ul>

To integrate the Task component into our Tasks component, we pass each task as a prop. This approach enables the Task component to dynamically render based on the properties of the task it receives. To ensure efficient management and updates of the list, we use the key prop to uniquely identify each task item in the list.

Post last updated on April 29, 2024