React Todo List Tutorial for Beginners (Step by Step)
Build a React todo list from scratch with useState, forms, and map(). Add, complete, and delete tasks with simple code and clear explanations. Start now.
Build a React todo list from scratch with useState, forms, and map(). Add, complete, and delete tasks with simple code and clear explanations. Start now.
Want to build a React todo list but not sure where to start? This guide shows you how, step by step. A todo list is the classic first React project because it uses the three ideas every React app needs: state (data that changes), a form (to add things), and a list (to show things). You need no experience beyond basic JavaScript.
By the end you will have a small app that can add tasks, mark them done, and delete them. Every code block is short, and each one is explained line by line right after it.
Your app will have an input box, an Add button, and a list of tasks. Each task will have a checkbox to mark it done and a Delete button. Here is the plan:
You need Node.js installed (download it from nodejs.org). Then open a terminal and run these commands. Vite is a free tool that creates a ready-to-use React project for you.
npm create vite@latest todo-app -- --template react
cd todo-app
npm install
npm run devThe first command creates a folder called todo-app. The second moves into it. The third downloads the code the project needs. The fourth starts a local server, and the terminal prints a link such as http://localhost:5173. Open it in your browser to see your app.
Now open the file src/App.jsx and delete everything inside it. We will write our own code there.
State is data that a component remembers. When state changes, React redraws the page for you. We use the useState hook to create it. A hook is a special React function that starts with the word use.
import { useState } from 'react';
function App() {
const [tasks, setTasks] = useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Build a todo app', done: false },
]);
return <h1>My Todo List</h1>;
}
export default App;Line 1 imports useState from React. Line 4 creates a state variable called tasks and a function called setTasks that changes it. The value inside useState( ) is the starting value: an array of two task objects. Each task has an id (a unique number), a text, and a done flag that says if it is finished. Line 9 returns what to show on screen. For more on this hook, see the useState guide linked below.
To display every task, we use the array method map(). It turns each item in the array into a piece of screen. Each item also needs a key prop, a unique value that helps React keep track of the items.
return (
<div>
<h1>My Todo List</h1>
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.text}</li>
))}
</ul>
</div>
);Replace the old return line with this. The curly braces { } let us write JavaScript inside the markup. tasks.map goes through the array one task at a time. For each task, we make an <li> (a list item) showing task.text. The key={task.id} gives each item its own unique label. You should now see your two tasks in the browser.
We need a second piece of state to hold what the user is typing. Then we need a function that adds a new task when the form is sent.
const [newTask, setNewTask] = useState('');
function handleSubmit(event) {
event.preventDefault();
if (newTask.trim() === '') return;
const task = { id: Date.now(), text: newTask, done: false };
setTasks([...tasks, task]);
setNewTask('');
}Put this code above the return line. newTask stores the current text of the input and starts empty. handleSubmit runs when the form is sent. event.preventDefault() stops the browser from reloading the page. The if line stops empty tasks (trim removes spaces at the ends). Date.now() gives a number that is different each time, so we use it as the id. [...tasks, task] builds a new array with all the old tasks plus the new one. We never change state directly; we always give React a new copy. Last, setNewTask('') clears the input.
Now add the form in the markup, just under the <h1>:
<form onSubmit={handleSubmit}>
<input
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="Add a task"
/>
<button type="submit">Add</button>
</form>onSubmit tells React to run handleSubmit when the form is sent. The input's value always comes from state, and onChange updates the state each time a key is typed. This pattern is called a controlled input. Type a task and press Add: it appears in the list.
Want to understand forms in more depth? Read how controlled components work in React.
React form handling for beginnersTo toggle a task, we make a new array where the clicked task has its done value flipped.
function toggleTask(id) {
setTasks(
tasks.map((task) =>
task.id === id ? { ...task, done: !task.done } : task
)
);
}toggleTask receives the id of the task that was clicked. We map over all tasks. If the task's id matches, we return a copy of it ({ ...task }) with done set to the opposite value (! means not). If it does not match, we return the task unchanged. Then setTasks saves the new array.
Now update the <li> so it shows a checkbox and a line through finished tasks:
<li key={task.id}>
<input
type="checkbox"
checked={task.done}
onChange={() => toggleTask(task.id)}
/>
<span style={{ textDecoration: task.done ? 'line-through' : 'none' }}>
{task.text}
</span>
</li>The checkbox is checked when task.done is true. When clicked, it calls toggleTask with this task's id. The span shows the text, and the style uses a ternary (a short if/else) to add a line through the text when the task is done.
Deleting means keeping every task except the one we want to remove. The array method filter() does exactly that.
function deleteTask(id) {
setTasks(tasks.filter((task) => task.id !== id));
}filter() keeps only the tasks where the test is true. Here the test is task.id !== id, so every task stays except the one with the matching id. Add a button inside the <li>, after the span:
<button onClick={() => deleteTask(task.id)}>Delete</button>Clicking the button runs deleteTask for that task. The task disappears from the list right away, because state changed and React redrew the page.
| Idea | Where you used it | What it does |
|---|---|---|
| useState | tasks and newTask | Remembers data and redraws the page when it changes |
| map() | Showing the list | Turns each array item into screen content |
| key prop | Each <li> | Helps React track items |
| Controlled input | The text box | Keeps the typed text in state |
| filter() | deleteTask | Removes an item by building a new array |
| Spread ... | Add and toggle | Copies an array or object without changing the original |
You can improve this app in many ways. Try adding an Edit button, a counter that shows how many tasks are left, or saving the tasks in the browser using localStorage. Each one teaches you something new while using the same basics.
Need a refresher on showing lists? See how map() and the key prop work.
Render a list in ReactStore an array of tasks in state with useState. Show it with map(), add tasks with a form that creates a new array, and remove tasks with filter(). This guide walks through each step with code.
You should know the basics: variables, functions, arrays, and objects. You do not need to be an expert. Every array method used here is explained as it appears.
React only redraws the page when you call the setter function with a new value. If you change the old array directly, React does not notice, and the screen stays the same. Always make a copy, for example with the spread syntax.
The key is a unique label that lets React tell items apart. When the list changes, React uses keys to update only the items that changed. Use a stable id, not the array index.
State resets on refresh. You can save the tasks to localStorage (a small storage space in the browser) and load them when the app starts. For shared data across devices, you would save them to a database.
Learning to code for a project or business idea? Fepiq builds fast, clean web apps and can help you plan yours.
Talk to FepiqOccasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.
Keep reading
Learn SQL CASE WHEN step by step. This beginner guide shows how to label, group and sort data with simple examples you can copy and run today.
PostgreSQL ILIKE explained for beginners: search text without worrying about case, learn % and _ wildcards, and copy simple examples you can run today.
Let's build something
Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.