Custom API local storage

AaksAaks
9 min read

During days 15 and 16 of my React journey, I worked on a project to create a todo list application. This project allowed me to implement Context API for state management and local storage for data persistence. Throughout the process, I learned how to structure components effectively, manage state without prop drilling, and ensure that data remains available even after a page refresh. This experience enhanced my understanding of React and its core concepts.

Step 1: Setting up the project using Vite and Tailwind

Step 2: Creating the Context Folder and TodoContext.js File
In the context file, we define the necessary functionalities but don’t implement them right away. We also set up value properties, which can later be accessed by any component that needs them. This way, we can easily share the values throughout the app.

import {createContext, useContext} from 'react'

export const TodoContext = createContext({
    //properties format (values)
    todos: [
        {
            id: "1",
            todo: "todo msg",
            completed: false,
        }
    ],
    //(methods)
    addTodo: (todo) => {},
    updateTodo: (todo, id) => {},
    deleteTodo: (id) => {},
    toggleComplete: (id) => {},
})


export const useTodo = () => {
    return useContext(TodoContext)
}

export const TodoProvider = TodoContext.Provider

Step 3: context/index.js
To simplify imports, we export everything from TodoContext.js through index.js. This way, we only need to import from index.js rather than importing each component separately every time.

export {TodoContext, TodoProvider, useTodo} from "./context/TodoContext"

Step 4: App.jsx
We’ll start by setting up the basic structure with some divs.

<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">
              {/* Todo form goes here */} 
            </div>
              <div className="flex flex-wrap gap-y-3">
                {/*Loop and Add TodoItem here */}
              </div>
        </div>
       </div>

Step 4: Adding Basic Functionality
Let’s start with the fundamental functionality of adding todos. We can implement this in App.jsx by wrapping everything in the TodoProvider from our context.

An important question is: what does TodoProvider provide? We can define this using value = {}. Essentially, we are destructuring, which allows us to access values using value. for everything we need.

Here, we have values from todos and methods such as addTodo, deleteTodo, updateTodo, and toggleComplete. We will wrap everything that is returned in App.jsx within the TodoProvider.

<TodoProvider value={{todos, addTodo, deleteTodo, updateTodo,
 toggleComplete}}>

</TodoProvider>

Step 5: Adding Functionality
In this step, we will add the new todos. The todo from addTodo will be added to the todos defined in useState. The todo value will be provided by a form that we will set up later.

Using prev, we will take the previous values, spread them out, and add our new todo. In the context file, we have Todo as an object that includes a unique ID. We will use Date.now() to generate this unique ID and then spread the other todo properties.

Important:
In the todos array in the context file, each todo is represented as a new object. To add a new todo, we need to create a new array that consists of the previous todos (...prev), which are the existing objects in the array, along with a new object for the current todo that requires a unique ID and other values defined in the context file. We will use (...todo) to spread all the properties, and for the value, we will utilize the input provided by the user.

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

 const addTodo = (todo) => {
    //setTodos(todo) //all the previous todo's will be removed and just the new one will be added

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


  }

Step 6: Updating Functionality
Here, todos is an array of todo items. We will loop through each todo and check if its ID matches the current ID of the todo that needs to be updated. If there’s a match, we’ll update it with the new todo details; if not, it will remain unchanged.

  const [todos, setTodos] = useState([])
  const updateTodo = (id, todo) => {
    setTodos((prev) => prev.map((eachVal) => eachVal.id === id
    ? todo 
    : prev))
  }

Step 7: Deleting Functionality
Using map is not the best optimization for deleting items. Instead, we will create a new array that contains all the todos except the one that is passed to the function for deletion (the one meant to be removed).

 const [todos, setTodos] = useState([]) 
    const deleteTodo = (id) => {
    setTodos((prev) => prev.filter((todo) => todo.id != id))
  }

Step 8: Toggling Completed Functionality
Using ...prev allows us to access the values (ID, todo, checked) from the context file. We will iterate through all the todos, and if we find a match for the ID, we will update each value of that todo by toggling the completed status.
!eachVal.completed because we’ve set completed: false

 const [todos, setTodos] = useState([])   
    const toggleComplete = (id) => {
        setTodos((prev) => prev.map((eachVal) => eachVal.id == id
        ? {...eachVal, completed: !eachVal.completed}
        : eachVal))
      }

With that, we have completed the basic functionality of the context. Now, we will move on to implementing the local storage functionality.


Local storage

In local storage, we use getItem and setItem. One issue is that all values are stored as strings, so we need to convert them to JSON format.

In our React app, if we have set up a todo list and added items to it, we want those todos to persist even after refreshing the page. To achieve this, we need a method that can access local storage and retrieve all the todos to populate our state. This way, after refreshing, all the previous todos will still be available in the list.

Step 1: Getting the items

 useEffect(() => {
    localStorage.getItem("todos")
  }, [])

Convert it into JSON

JSON.parse(localStorage.getItem("todos"))

Store it in a variable

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

Step 2: Setting the Items (If There Are Any)
To begin, we use localStorage.getItem("key") to retrieve existing items.

We will also implement another feature for local storage. As mentioned earlier, when the page loads, all values will be retrieved. Now, we need to ensure that when a new todo is added, it is also saved to local storage.

After adding a new todo to our useState todos array, we should update local storage with the new value. We do this using localStorage.setItem("key", "value").

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

Making Components

Todo Form:

We’ll start with the basic template for the Todo Form as follows:

function TodoForm() {
     return (
        <form  className="">
            <input type="text" placeholder="Write Todo..."/>
            <button type="submit">
                Add
            </button>
        </form>
    );
}
export default TodoForm;

Step 1: Adding a State for single todo

const [todo, setTodo] = useState("")

Step 2: Utilizing Context
Here, we will apply the context to use the addTodo functionality. Since we have set up context, there's no need for prop drilling. We can directly import useTodo from the context.jsx file to access our functionalities. These functionalities are defined in the context file but will be implemented in App.jsx

const {addTodo} = useTodo

Creating a method for that

 const {addTodo} = useTodo()

    const add = (e) => {
        e.preventDefault()

        if(!todo) return 
        // addTodo(todo) wrong approach
        addTodo({todo, completed:false})
        setTodo("")
    }

We need to clarify that using addTodo(todo) is not the correct approach here. When defining this functionality, we stated that it expects an object, which should be spread out. Therefore, we need to provide an object containing id, todo, and completed.

Since the id is already defined as Date.now() within the functionality, there's no need to write that again. Additionally, when both the key and value are the same (i.e., todo: todo), we can simply write todo instead.

Lastly, we need to ensure that setTodo("") is cleared after the todo is added.

Now, let's implement the onSubmit handler on our form to activate the addTodo method. This will ensure that when the form is submitted, the todo gets added.

Next, we will wire the input by setting value={todo} from our useState.

To handle any changes in the input, we will use onChange to update the state accordingly.

return(
    <form onSubmit = {add}>
        <input type="text" placeholder="Write Todo..."
        value = {todo}
        onChange = {(e) => setTodo(e.target.value)}
        />
            <button type="submit">
                Add
            </button>
    </form>
)

TodoItem

We’ll start with the basic template for the Todo Form as follows:

import React from 'react'

function TodoItem({ todo }) {
     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;

Once an item is marked as completed, it should no longer be editable, as the state is preserved. If an item is edited, we need to update the message, which will also require state management.

Additionally, if an item is deleted, the delete functionality should be implemented as well.

First, we need to set up the context in the above code, so we'll add that accordingly.

const {updateTodo, deleteTodo, toggleCompleted} = useTodo

Another State

const [todoMsg, setTodoMsg] = useState(todo.todo)

Update
The function will take the id and todo as parameters. Since todo is an object, we need to encapsulate it within curly braces {}.

To update the todo, we will spread the existing todo object and only change the value of "todo: msg" within it.

 const editTodo = () => {
        updateTodo(todo.id, {...todo, todo: todoMsg})
        setIsTodoEditable(false)    
    }

Completed functionality

const toggleCompleted = () => {
        toggleComplete(todo.id)
    }

Final Step (in App.jsx)

  1. TodoForm
    To include the Todo Form in our main application, we will add the following line in App.jsx:
    <TodoForm/>

  2. TodoItems
    For displaying the list of todos, we will implement a loop. It's important to assign a unique key to each item in the list for optimal performance and to help React identify which items have changed.

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

In this project, I learned how to effectively use Context in React to manage state and implement functionalities like adding, updating, deleting, and toggling todos. I now understand the concepts of local storage and how to integrate it into a React application. However, I realize that I need to implement these concepts in multiple projects to become more comfortable and confident with them.

0
Subscribe to my newsletter

Read articles from Aaks directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aaks
Aaks