Custom API local storage
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 div
s.
<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)
TodoForm
To include the Todo Form in our main application, we will add the following line inApp.jsx
:
<TodoForm/>
TodoItems
For displaying the list of todos, we will implement a loop. It's important to assign a uniquekey
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.
Subscribe to my newsletter
Read articles from Aaks directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by