Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PC×
Skip to content
World desk8 min

How to Build a Task Management App with React & TypeScript

Build a working task list in React and TypeScript: a typed Task model, immutable add, toggle, and delete handlers, component structure, optional filters, and the type-check step Vite does not provide.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A usable task list in React and TypeScript needs three things: a typed task shape, one array of tasks kept in component state, and handlers that replace that array with an updated copy whenever a task is added, completed, or deleted. This tutorial builds that version step by step, adds an optional filter view, and explains where the app stops being a starting point.

What you will build

The finished interface lets a user type a task title and add it to a list, check a task off as completed, delete a task, and switch the list between all, active, and completed tasks. It also shows a count of tasks still remaining. Every one of these behaviors runs on the same in-memory array of tasks, with no server, database, or sync.

Deliberately left out of this version: editing a task’s title, due dates, priorities, undo, and any form of saving. Each of those can be added later, but none is required to understand the core pattern.

Set up the project

Choose a React setup that supports TypeScript

The TypeScript handbook’s React guidance notes that TypeScript supports JSX and can model common React patterns such as useState. It names Create React App, Next.js, and Gatsby as examples of tools that support TypeScript out of the box. That list describes what the documentation covers; it is not a ranking, so pick the setup that matches the scope of your tutorial.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Before running any installation command, open the official starter guide for the framework you chose and follow its current instructions. Starter tooling and version numbers change, so this article deliberately does not reproduce them.

Use .tsx files and a matching jsx compiler option

Any file that contains JSX must use the .tsx extension. TypeScript also needs the jsx compiler option set to one of its supported modes: preserve, react, react-jsx, react-jsxdev, or react-native. The correct value depends on your bundler or framework, so check the tsconfig.json that your starter generated instead of copying a value from another project. The TypeScript JSX reference explains each mode.

Add a separate type-check step when using Vite

If you build with Vite, transpilation alone will not catch type errors. TypeScript’s build-tools guidance states: “Vite supports importing .ts files out-of-the-box. It only performs transpilation and not type checking.” Your dev server can run happily while the types are wrong.

Add a script to package.json that runs the TypeScript compiler in check-only mode (tsc --noEmit is the usual form), and run it locally and in continuous integration alongside the build. A passing build is not evidence that the types are correct.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Know where React type declarations come from

TypeScript’s type declarations reference explains that packages can bundle their own declarations. If your React setup does not bundle them, the @types/react package supplies them, and TypeScript automatically discovers declarations under node_modules/@types. Check your starter’s dependencies first; you may not need to install it yourself.

Model the smallest useful task

A task needs three fields: a stable identifier, a human-readable title, and a completion flag. Due dates or priorities belong only if your interface actually displays and changes them.

type Task = {
  id: string;
  title: string;
  completed: boolean;
};

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

The id is what lets you find a task later, even after other tasks are added or removed. Generate it once, when the task is created. In a browser, crypto.randomUUID() is a straightforward option, though it is only available in secure contexts such as https pages and localhost. Never derive the id from the array position, because positions shift when items are deleted.

Keep exactly one array of tasks as the source of truth. Do not store separate arrays for active and completed tasks. Compute those views from the main array on each render. Storing copies invites them to drift out of sync, a risk the React guide on choosing the state structure warns against.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Split the interface into components

The app uses four components. The top-level App owns the state and every handler; the rest receive data and callbacks as props.

Component Owns Receives as props Responsibility
App The tasks array and the filter value Nothing Defines add, toggle, and delete; computes visible and remaining tasks
TaskForm The draft title text while typing onAdd Collects a title and submits it
TaskList Nothing Visible tasks, onToggle, onDelete Renders one row per visible task
TaskItem Nothing One task, onToggle, onDelete Renders a checkbox and delete button for one task

The draft title is the only state that sits outside App, because nothing else needs it. Lifting it upward would add props with no benefit. The task array and filter, by contrast, must live in App because the form, the list, and the summary all depend on them. This is the pattern React documents as sharing state between components: move state to the nearest common parent and pass it down.

A third-party state library is not needed for an app this size. React’s built-in state and props are enough, and the sample app does not depend on one.

Add, complete, and delete tasks without mutating state

React treats the state array as read-only. Calling push, splice, or assigning into an element changes the array in place, and React may not re-render. Instead, each handler builds a new array. The React guide on updating arrays in state covers these patterns in detail.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The complete component below is a working starting point. It has not been run inside a specific starter template, so adjust imports and file locations to match your project.

import { useState } from "react";

type Task = {
  id: string;
  title: string;
  completed: boolean;
};

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

export default function App() {
  const [tasks, setTasks] = useState([] as Task[]);
  const [filter, setFilter] = useState("all" as Filter);

  function addTask(rawTitle: string): boolean {
    const title = rawTitle.trim();
    if (title === "") return false;
    const newTask: Task = {
      id: crypto.randomUUID(),
      title,
      completed: false,
    };
    setTasks((prev) => [...prev, newTask]);
    return true;
  }

  function toggleTask(id: string) {
    setTasks((prev) =>
      prev.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    );
  }

  function deleteTask(id: string) {
    setTasks((prev) => prev.filter((task) => task.id !== id));
  }

  const visibleTasks = tasks.filter((task) => {
    if (filter === "active") return !task.completed;
    if (filter === "completed") return task.completed;
    return true;
  });

  const remaining = tasks.filter((task) => !task.completed).length;

  return (
    <main>
      <h1>Tasks</h1>
      <TaskForm onAdd={addTask} />
      <fieldset>
        <legend>Show</legend>
        <label>
          <input
            type="radio"
            name="filter"
            checked={filter === "all"}
            onChange={() => setFilter("all")}
          />
          All
        </label>
        <label>
          <input
            type="radio"
            name="filter"
            checked={filter === "active"}
            onChange={() => setFilter("active")}
          />
          Active
        </label>
        <label>
          <input
            type="radio"
            name="filter"
            checked={filter === "completed"}
            onChange={() => setFilter("completed")}
          />
          Completed
        </label>
      </fieldset>
      <p>{remaining} task{remaining === 1 ? "" : "s"} remaining</p>
      <ul>
        {visibleTasks.map((task) => (
          <TaskItem
            key={task.id}
            task={task}
            onToggle={toggleTask}
            onDelete={deleteTask}
          />
        ))}
      </ul>
    </main>
  );
}

type TaskFormProps = {
  onAdd: (title: string) => boolean;
};

function TaskForm({ onAdd }: TaskFormProps) {
  const [draft, setDraft] = useState("");

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        if (onAdd(draft)) setDraft("");
      }}
    >
      <label htmlFor="new-task">New task</label>
      <input
        id="new-task"
        value={draft}
        onChange={(event) => setDraft(event.target.value)}
      />
      <button type="submit">Add task</button>
    </form>
  );
}

type TaskItemProps = {
  task: Task;
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
};

function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
  return (
    <li>
      <label>
        <input
          type="checkbox"
          checked={task.completed}
          onChange={() => onToggle(task.id)}
        />
        <span>
          {task.title}
          {task.completed ? " (completed)" : ""}
        </span>
      </label>
      <button type="button" onClick={() => onDelete(task.id)}>
        Delete {task.title}
      </button>
    </li>
  );
}

What each handler does to the user

  • Add: the title is trimmed, and a blank or whitespace-only entry is rejected without changing the list. A valid title creates a task with a new id and completed: false, appended to the end. The draft field clears only after a successful add.
  • Toggle: the matching task is replaced by a copy with its completed value flipped. Its id and title stay the same, so the row keeps its identity in the list.
  • Delete: the matching task is removed by filtering it out of a new array. No other task changes.
  • Filter and count: the visible list and the remaining count are computed from tasks on each render. They are not stored anywhere.

The key on each TaskItem uses the stable task id. Using the array index instead would cause rows to be reused for the wrong tasks after a deletion.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Keep the interface usable without relying on color

Several details in the example are chosen for accessibility rather than appearance. The title input has a visible label tied to it with htmlFor, and the checkbox sits inside its label so clicking the text toggles it. The completed state is written as text, not only shown with a strike-through or color. Each delete button names the task it removes, so a screen reader user hears which row is affected.

These are reasonable implementation choices, not a formal conformance check. If you need to meet a specific standard, consult the W3C Web Accessibility Initiative guidance and test with assistive technology.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshoot common problems

  • JSX produces type errors or the compiler rejects angle-bracket syntax: confirm the file ends in .tsx and that the jsx option in tsconfig.json matches what your starter configured.
  • The app runs but the editor or CI reports type errors: your bundler is transpiling only. Run the type-check script you added.
  • The compiler cannot find React’s types: check whether your setup bundles them. If not, confirm @types/react is installed and listed in your dependencies.
  • Clicking one checkbox changes another row: the id is probably not unique or stable, or the row key is the array index. Use the id from creation for both.
  • A task does not update on screen after a change: the state was mutated in place. Replace the array in each handler as shown above.
  • crypto.randomUUID is undefined: the page is not in a secure context or the runtime is older. Serve the app over https or from localhost, or generate ids another way that stays unique for the life of the list.

Where this version stops

All tasks live in component memory. When the page reloads, the list starts empty again. Saving tasks requires a separate storage layer, such as the browser’s local storage or a backend, and that choice affects how ids are generated, how conflicts are resolved, and how errors are handled. This tutorial does not implement either, and it does not provide undo, synchronization between devices, or offline support.

Once the pattern in this article works, the natural next steps are adding an edit handler that follows the same map-and-replace approach, and moving the task array into a storage-backed hook without changing the components.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.