← Back to Solutions

Build a Simple To-Do App with React

By Edvvo | 2026
Build and test a small React study-task list with complete source code, independent task IDs and clear limits on data persistence.

Build a small, testable study-task app

This exercise creates an in-memory list with add, complete and remove actions. You need basic JavaScript arrays, functions and JSX. It deliberately does not include accounts, reminders or saved data: refreshing the page clears the list.

Set up the project

Install a supported Node.js LTS release compatible with the current Vite requirements. Follow the official React setup reference below if your environment differs. Run these commands in a terminal:

npm create vite@latest study-tasks -- --template react
cd study-tasks
npm install
npm run dev

Open the local URL printed by the development server. Replace src/App.jsx with the following component:

import { useState } from 'react';

export default function App() {
  const [title, setTitle] = useState('');
  const [tasks, setTasks] = useState([]);
  function addTask(event) {
    event.preventDefault();
    const text = title.trim();
    if (!text) return;
    setTasks(current => [...current, {
      id: crypto.randomUUID(), title: text, done: false
    }]);
    setTitle('');
  }
  return <main>
    <h1>My study tasks</h1>
    <form onSubmit={addTask}>
      <label htmlFor="task-title">New task</label>
      <input id="task-title" value={title}
        onChange={event => setTitle(event.target.value)} />
      <button type="submit">Add task</button>
    </form>
    <p>{tasks.filter(task => task.done).length} of {tasks.length} complete</p>
    <ul>{tasks.map(task => <li key={task.id}>
      <label><input type="checkbox" checked={task.done}
        onChange={() => setTasks(current => current.map(item =>
          item.id === task.id ? { ...item, done: !item.done } : item
        ))} />{task.title}</label>
      <button type="button" aria-label={'Remove ' + task.title}
        onClick={() => setTasks(current =>
          current.filter(item => item.id !== task.id)
        )}>Remove</button>
    </li>)}</ul>
  </main>;
}

Understand the state changes

The input has its own state so typing does not create a task. Submitting trims whitespace and appends a new object. Each task gets an ID independent of its title, allowing two tasks with the same words to behave independently. The checkbox maps the current array into a new array and changes only the matching object. Removal filters by ID rather than by title.

Test before adding features

  • Submit spaces only: the list should stay empty.
  • Add two tasks named Read chapter one: both should appear.
  • Complete the second task: the counter should show 1 of 2 complete.
  • Remove the first task: the remaining task should still be checked.
  • Refresh: the list should become empty because this example has no persistence.

Troubleshooting and next exercise

If the terminal reports an unsupported engine, check the Node version against the build tool requirements rather than installing an obsolete release. If the page is blank, check the browser console and compare the closing JSX tags. Run npm run build to check the production bundle.

As a next exercise, add a button that filters the displayed list to incomplete tasks. Keep the original tasks array intact so completed tasks can be shown again. Only then add storage, including a plan for invalid stored data and a clear explanation of where personal task text is saved.

ReactTo-DoAppGuide

Your privacy choices

We use optional analytics to understand visits and advertising to support Edvvo. These services stay off unless you accept. Your study tools work either way. Read our privacy policy.