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

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A usable first task app needs four things: a typed task shape, one place where the task list lives, updates that create new arrays instead of editing the old one, and a type check that runs separately from your dev server. This guide builds that version: you can add tasks, mark them complete, delete them, and filter the list. The tasks live in memory, so a page reload clears them. Saving tasks to local storage or a backend is a separate step that this article does not cover.

What this first version includes

  • Typed tasks with a stable id, a title, and a completed flag.
  • A form that adds a task when the title is not empty after trimming spaces.
  • A checkbox that toggles completion without changing the task’s identity.
  • A delete button for each task.
  • A filter (All, Active, Completed) and a count of remaining tasks, both derived from the same list.

Undo, editing titles, due dates, priorities, syncing between devices, and offline use are not part of this version. Adding any of them later means extending the same task type and update functions shown here.

Step 1: Choose a project setup

TypeScript’s React guidance states that TypeScript supports JSX and can model common React patterns such as useState. The same page lists several starter setups, including Create React App, Next.js, and Gatsby. That list describes what the documentation covers; it is not a ranking of tools. Pick the setup that fits your tutorial’s scope, then follow its current official starter instructions. The React Quick Start page is the place to start for the React side. Starter commands and package versions change over time, so check them on the framework’s own site when you set up the project rather than copying them from older articles.

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

JSX files and the jsx compiler option

Components that contain JSX must use the .tsx extension. TypeScript’s JSX reference lists the jsx compiler modes it supports: preserve, react, react-jsx, react-jsxdev, and react-native. The correct value depends on how your toolchain transforms JSX. Your starter’s tsconfig.json usually sets this already; leave it alone unless your build tool requires a different mode.

Vite needs a separate type-check step

If your project uses Vite, the build can succeed even when types are wrong. TypeScript’s build-tools guidance states: “Vite supports importing .ts files out-of-the-box. It only performs transpilation and not type checking.” Add a script that runs the TypeScript compiler without emitting files, such as tsc --noEmit, and run it in your editor workflow or continuous integration. The exact script name depends on your template, so check its package.json.

Type declarations for React

TypeScript looks for type declarations in two places: packages that ship their own declarations, and packages under node_modules/@types. If your React setup does not include React’s declarations, the @types/react package supplies them. Check whether your starter already installs it before adding it. The type declarations handbook page explains how this discovery works.

Step 2: Model the task

A task needs an identifier that stays the same as the list changes, a title, and a completion flag. Store only those values. Visible lists, counts, and filters can all be calculated from the single array, so you avoid keeping copies that can drift apart. React’s guidance on choosing the state structure makes this point: avoid redundant or duplicated state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
// src/types.ts
export type Task = {
  id: string;
  title: string;
  completed: boolean;
};

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

For IDs, crypto.randomUUID() is a simple option. It is available in current browsers, but only in secure contexts, which includes localhost during development and HTTPS pages. On a plain-HTTP page at any other address, the call is unavailable, so use a counter kept in a useRef instead in that case.

Step 3: Split the interface and place the state

A workable structure has four pieces:

  • App owns the task array and the current filter.
  • AddTaskForm holds the text being typed and reports a finished title upward.
  • TaskList renders the visible tasks.
  • TaskRow renders one task and its checkbox and delete button.

The task array belongs in App because the form, the list, and the filter all need it. React’s guide on sharing state between components describes this pattern: move state to the nearest common parent, then pass values and event handlers down as props. A small app like this does not need a global state library, and React’s built-in state is enough.

Step 4: Update the task array immutably

React compares state by reference. If you push into the existing array or change a task object in place, React may not re-render the list. Instead, each update returns a new array. React’s guide to updating arrays in state covers the same techniques used below.

Adding a task

The form trims whitespace, ignores empty titles, sends the cleaned title to the parent, and clears the input. The parent creates the task and appends it to a copy of the array.

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.
// src/App.tsx (inside App)
function addTask(title: string) {
  const newTask: Task = {
    id: crypto.randomUUID(),
    title,
    completed: false,
  };
  setTasks((prev) => [...prev, newTask]);
}

Toggling completion

The update finds the task by id and returns a new object for that task only. Every other task object is reused unchanged. The task keeps its identity, so the row and its checkbox stay the same element on screen.

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

Deleting a task

filter returns a new array without the matching task. Deletion removes only that item; the other tasks keep their order.

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

Step 5: Derive the filter and the summary

Keep filter as its own state and compute what to display during render. Nothing about the visible list or the count needs to be stored.

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;

Pass visibleTasks to TaskList and show remaining as text, such as “3 tasks left.” Mark the active filter button with aria-pressed so assistive technology can report which filter is on.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Step 6: Wire the pieces together

// src/AddTaskForm.tsx
import { useState, type FormEvent } from "react";

type Props = { onAdd: (title: string) => void };

export function AddTaskForm({ onAdd }: Props) {
  const [title, setTitle] = useState("");

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const trimmed = title.trim();
    if (trimmed === "") return;
    onAdd(trimmed);
    setTitle("");
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="new-task">New task</label>
      <input
        id="new-task"
        value={title}
        onChange={(event) => setTitle(event.target.value)}
      />
      <button type="submit">Add task</button>
    </form>
  );
}
// src/TaskRow.tsx
import type { Task } from "./types";

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

export function TaskRow({ task, onToggle, onDelete }: Props) {
  return (
    <li>
      <label>
        <input
          type="checkbox"
          checked={task.completed}
          onChange={() => onToggle(task.id)}
        />
        <span style={{ textDecoration: task.completed ? "line-through" : "none" }}>
          {task.title}
        </span>
      </label>
      <button
        type="button"
        onClick={() => onDelete(task.id)}
        aria-label={`Delete ${task.title}`}
      >
        Delete
      </button>
    </li>
  );
}
// src/TaskList.tsx
import { TaskRow } from "./TaskRow";
import type { Task } from "./types";

type Props = {
  tasks: Task[];
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
};

export function TaskList({ tasks, onToggle, onDelete }: Props) {
  if (tasks.length === 0) return <p>No tasks to show.</p>;

  return (
    <ul>
      {tasks.map((task) => (
        <TaskRow
          key={task.id}
          task={task}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  );
}

In App, render AddTaskForm with onAdd={addTask}, the filter buttons, TaskList with tasks={visibleTasks}, onToggle={toggleTask}, and onDelete={deleteTask}, and the remaining count. The completed text uses a line-through and the checked checkbox, so the state does not depend on color alone.

Accessibility basics for this app

  • Every input has a visible label, connected with htmlFor and id, or wrapped in a label.
  • Icon-only or generic buttons need a descriptive name. The delete button above uses aria-label to include the task title, so a screen reader user hears which task will be removed.
  • Completion is shown by the checkbox state and by strikethrough text, not by color alone.
  • Use real button and input elements rather than clickable div elements, so keyboard focus and activation work by default.

Troubleshooting

  • The list does not update after a change. Check that the update returns a new array, for example with map, filter, or a spread, instead of calling push or changing a task object in place.
  • Rows flicker, or the wrong task changes. Confirm each row’s key is the task id, not the array index, and that the ID is generated once when the task is created.
  • JSX produces syntax errors. Confirm the file ends in .tsx and that the jsx setting matches your starter’s configuration.
  • The app runs but type errors appear only later. If you use Vite, run your type-check script. Transpilation alone does not report type errors.
  • TypeScript cannot find React’s types. Check whether your setup already provides them. If not, the @types/react package is the usual source.

Next steps

Once the in-memory version works, add persistence as a separate layer. Load the initial array from storage, write it back after each change, and keep the update functions as they are. The state shape in Step 2 does not need to change for that.

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.