Creating a to-do list app using React and local storage is a practical way to learn essential React concepts such as components, state management, and the useEffect hook, while also understanding how to persist data across browser sessions. This article walks through the process of designing and coding a simple yet fully functional to-do list app that stores user tasks locally on their device.
Why Use React and Local Storage?
React is a popular JavaScript library for building user interfaces. Its component-based architecture makes it easy to build dynamic and interactive applications. On the other hand, local storage is a Web API that allows developers to store key-value pairs in a user’s browser without any expiration. This makes it ideal for simple apps that don’t require a back-end server.

By using both technologies, developers can create responsive applications that load quickly and work offline, providing a seamless user experience.
Setting Up the Environment
First, ensure that Node.js and npm are installed on your system. Then, create a new React app using the following command:
npx create-react-app todo-list-app
Once the setup is complete, navigate into your project folder:
cd todo-list-app
Start the development server:
npm start
Creating the Components
The to-do list application will consist of the following components:
- App – The main component that holds the entire application
- TodoList – Displays the list of tasks
- TodoItem – Represents a single task item
- AddTodo – Contains the form to add new tasks
App Component
This is the root component of the app. It manages the state for all the to-dos and handles lifecycle events:
import React, { useState, useEffect } from 'react';
import AddTodo from './AddTodo';
import TodoList from './TodoList';
function App() {
const [todos, setTodos] = useState([]);
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem('todos'));
if (storedTodos) {
setTodos(storedTodos);
}
}, []);
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = (text) => {
setTodos([...todos, { text, completed: false }]);
};
const toggleTodo = (index) => {
const newTodos = [...todos];
newTodos[index].completed = !newTodos[index].completed;
setTodos(newTodos);
};
const deleteTodo = (index) => {
const newTodos = todos.filter((_, todoIndex) => todoIndex !== index);
setTodos(newTodos);
};
return (
<div className="App">
<h1>My To-Do List</h1>
<AddTodo addTodo={addTodo} />
<TodoList
todos={todos}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
/>
</div>
);
}
export default App;
AddTodo Component
This component provides an input field and a button to add new items.
import React, { useState } from 'react';
function AddTodo({ addTodo }) {
const [value, setValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!value.trim()) return;
addTodo(value);
setValue('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Add a task"
/>
<button type="submit">Add</button>
</form>
);
}
export default AddTodo;
TodoList and TodoItem Components
The TodoList loops over all tasks and renders a TodoItem for each one.
import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, toggleTodo, deleteTodo }) {
return (
<ul>
{todos.map((todo, index) => (
<TodoItem
key={index}
todo={todo}
index={index}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
/>
))}
</ul>
);
}
export default TodoList;
import React from 'react';
function TodoItem({ todo, index, toggleTodo, deleteTodo }) {
return (
<li style={{ textDecoration: todo.completed ? 'line-through' : '' }}>
{todo.text}
<button onClick={() => toggleTodo(index)}>Toggle</button>
<button onClick={() => deleteTodo(index)}>Delete</button>
</li>
);
}
export default TodoItem;
Enhancing the User Experience
Once the basic structure is in place, consider enhancing the UI using CSS or adding animations. Using tools like TailwindCSS or Bootstrap can significantly improve the look and feel of your app.

Another good improvement would be to add a filter that lets users view items that are completed or still pending.
Persisting Data with Local Storage
The use of useEffect
ensures that the application data is synced to local storage every time the state changes. That way, a refresh or browser close won’t result in losing the task list.
It’s important to remember that local storage stores data as strings. Hence, data must be serialized using JSON.stringify
before saving and parsed back using JSON.parse
when retrieving.
Conclusion
Developing a to-do list app using React and local storage is an excellent project for practicing modern frontend development skills. It allows developers to explore concepts like component composition, state management, and data persistence, all while creating a useful application.
This project can also serve as a foundation. Developers may expand it by integrating backend services, useContext, Redux, or even advanced features like drag-and-drop, reminders, or deadline alerts.
Frequently Asked Questions
-
Q: Does this app work without an internet connection?
A: Yes, because the app uses local storage, it works entirely in the browser without needing a network after the initial load. -
Q: What happens to the tasks if I close the browser?
A: Since tasks are saved in local storage, they will persist between sessions unless the user clears their browser cache. -
Q: Is local storage secure?
A: Local storage is not encrypted and can be accessed through browser developer tools. It’s not recommended for storing sensitive data. -
Q: Can I deploy this app online?
A: Yes, the app can be deployed using platforms like Netlify, Vercel, or GitHub Pages. -
Q: How can I add categories or deadlines to tasks?
A: You can modify the task object structure to include additional fields likecategory
anddueDate
, and adjust your components accordingly.