Free to list, always.No paid rankings. Every recommendation explains its trade-offs.
OpenSourceChoice
Frontend · Intermediate

Building a Todo App with React and TypeScript

Build a typed React and TypeScript Todo app with safe state updates, reusable components, persistence, tests, and production checks.

2 hoursUpdated Jul 21, 2026285 views

Build a typed Todo app in React + TypeScript: model data, render lists, and keep state updates safe.

What you will learn

  • Scaffold a Vite + React + TypeScript app
  • Define a Todo type and keep updates immutable
  • Add, toggle, and filter todos
  • Separate presentational components from state

Prerequisites

  • Node.js 20+
  • Basic React and TypeScript

Scaffold

npm create vite@latest todo-ts -- --template react-ts
cd todo-ts
npm install
npm run dev

Vite guide: Vite — Getting Started. React TypeScript tips: React + TypeScript cheatsheet.

Model the data

export type Todo = {
  id: string;
  text: string;
  completed: boolean;
};

Minimal app state

import { useMemo, useState } from "react";
import type { Todo } from "./types";

export default function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [text, setText] = useState("");
  const remaining = useMemo(() => todos.filter((todo) => !todo.completed).length, [todos]);

  function addTodo() {
    const value = text.trim();
    if (!value) return;
    setTodos((current) => [...current, { id: crypto.randomUUID(), text: value, completed: false }]);
    setText("");
  }

  return (
    <main>
      <h1>Todos ({remaining} left)</h1>
      <input value={text} onChange={(event) => setText(event.target.value)} onKeyDown={(event) => event.key === "Enter" && addTodo()} />
      <button type="button" onClick={addTodo}>Add</button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <label>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => setTodos((current) => current.map((item) => item.id === todo.id ? { ...item, completed: !item.completed } : item))}
              />
              {todo.text}
            </label>
          </li>
        ))}
      </ul>
    </main>
  );
}

Persist and filter (next slice)

type Filter = "all" | "active" | "completed";

// after todos change:
localStorage.setItem("todos", JSON.stringify(todos));

// on first render:
const [todos, setTodos] = useState<Todo[]>(() => {
  try {
    return JSON.parse(localStorage.getItem("todos") ?? "[]") as Todo[];
  } catch {
    return [];
  }
});

Filter with a derived list: todos.filter(...) based on a Filter state—do not keep a second “visibleTodos” array in sync by hand.

Next improvements

  • Extract TodoList and TodoItem components
  • Add Vitest + Testing Library for add/toggle
  • Disable Add when the input is empty
  • Announce remaining count to assistive tech

Practice checklist

  • Add, toggle, and delete a todo without TypeScript errors
  • Reload the page and keep todos via localStorage
  • Add All / Active / Completed filters

Sources and further reading

Who this guide is for

  • Developers learning React and TypeScript together
  • Frontend engineers practicing predictable state updates
  • Teams needing a small reference app for component and test conventions

Definition of done

An accessible Todo application with typed domain data, add/toggle/delete/filter flows, safe persistence, empty-state handling, and tests for the primary user journey.

Finishing the commands is not the same as finishing the guide. Keep the result only when another person can reproduce it, inspect the evidence, and understand the remaining risks.

Professional workflow

Phase 1 — Define the product slice

Goal: Build one complete workflow before adding architecture.

  • Write the Todo and Filter types.
  • Define acceptance criteria for add, toggle, delete, and filter.
  • Choose stable IDs and a single owner for the collection.

Verification: Every acceptance criterion maps to a visible interaction and expected screen state.

Phase 2 — Build accessible interactions

Goal: Make the app usable without relying on pointer-only behavior.

  • Use a labeled form so Enter submits naturally.
  • Connect each checkbox to visible text.
  • Announce remaining-item changes without stealing focus.

Verification: The full workflow works using only the keyboard and has no unlabeled controls.

Phase 3 — Persist and protect data

Goal: Treat browser storage as an unreliable boundary.

  • Validate parsed storage before using it.
  • Version the stored shape if future changes are likely.
  • Provide a clear recovery path for malformed data.

Verification: Corrupted storage produces a safe empty state rather than a broken application.

Review questions before you continue

  • Which fields belong to the domain rather than the UI?
  • Can filtered items be derived instead of synchronized?
  • What should happen when text is blank or duplicated?
  • How will persisted data migrate when the model changes?

If an answer is unknown, record it as an explicit follow-up instead of hiding the uncertainty behind a successful demo.

Troubleshooting decision guide

  • List items update incorrectly: verify immutable updates and stable keys.
  • TypeScript widens a filter to string: declare the union at the state boundary.
  • Saved data crashes startup: parse inside a guarded initializer and validate the result.
  • Focus becomes confusing after delete: choose and test a deliberate focus destination.

Change one variable at a time, preserve the first useful error, and write down the command or condition that fixed it. That turns a private debugging session into team knowledge.

Production-readiness checklist

  • Validate all external data, including localStorage.
  • Add component tests for the critical user path.
  • Include empty, error, and long-text visual states.
  • Check contrast, labels, focus order, and reduced motion.
  • Avoid adding global state until multiple distant consumers genuinely need it.

These checks are intentionally stricter than a beginner demo. Use them to decide whether to continue learning, run a controlled pilot, or request specialist review.

Your next 30 minutes

  • Complete the keyboard-only user journey.
  • Add one test covering add, toggle, filter, and reload.
  • Document the stored schema and recovery behavior.
  • Use the finished app as a template for your next bounded React feature.

CTA: Pick the first unchecked step, complete it now, and save the evidence in your project README or decision log. Then open the official documentation linked below before adopting the result in production.

Similar guides

Keep learning in the same lane.

Matched by technology, category, tags, and connected software—not ranked sponsorship.

BeginnerGetting Started with React Hooks

Use React Hooks for state, effects, derived values, and reusable logic while avoiding common dependency and cleanup bugs.

React · 1 hour
IntermediateModern JavaScript: ES6 and Beyond

Learn modern JavaScript modules, destructuring, async/await, safer defaults, and review practices you can apply to current codebases.

JavaScript · 2 hours
BeginnerCSS Grid Layout: A Complete Guide

Master CSS Grid tracks, placement, responsive patterns, accessibility checks, and debugging techniques using standards-based examples.

CSS · 2 hours
IntermediateBuilding a React Native App from Scratch

Build a React Native app with Expo, typed navigation, reusable screens, device testing, accessibility checks, and release preparation.

React Native · 4 hours
Browse the full library →
Turn reading into evidence

Make this guide useful today.

Complete one verification step, save the result in your project notes, and decide whether you are ready for the next phase. A reproducible result is more valuable than a finished page.

  • Run the smallest working example.
  • Test one failure or edge case.
  • Record the command, result, and remaining risk.