Build a Task Tracker with React

Build a React task tracker app with components, state, and props.

Introduction

30 Second Summary

Everyone has a mental list of things they need to get done. The challenge is turning that messy list into something visual, interactive, and satisfying to check off.

In this project, you will build a Task Tracker app using React that lets you add tasks, mark them as complete, and delete them.

What You'll Build

A single-page Task Tracker application built with multiple React components that pass data and callbacks via props.

By the end of this project, you'll have:

  • A React development environment set up with Vite that you can reuse for future projects.
  • A working app with multiple components (App, TaskForm, TaskList, TaskItem) that communicate through props.
  • Hands-on experience with state management using React's useState hook to handle adding, toggling, and deleting tasks.
  • Secret Mission: Add a filter bar that lets you toggle between viewing All, Active, and Completed tasks.

Are there any prerequisites?

A basic understanding of HTML, CSS, and JavaScript fundamentals is helpful. You don't need any prior React experience.

Set Up Your Development Environment

Before you can build the Task Tracker app, you need three things on your machine: a JavaScript runtime, a code editor, and a project scaffold.

In this step, you'll install Node.js (which runs JavaScript outside the browser), set up VS Code as your editor, and use Vite to generate a ready-to-go React project with TypeScript.

In this step, get ready to:
  • Install Node.js and verify it works.
  • Install VS Code as your code editor.
  • Scaffold a React + TypeScript project with Vite.
Install Node.js and verify

Node.js lets you run JavaScript on your computer instead of only in a browser. It also comes with npm, a package manager you'll use to install libraries and scaffold projects.

  • Open https://nodejs.org in your browser.
  • Click the LTS download button. This gives you the stable, long-term support version.
  • Run the downloaded installer and accept all the default options.

Once the installer finishes, confirm Node.js is working by opening a terminal.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check your Node.js version by running this command:
node --version
  • Check your npm version by running:
npm --version

You should see a version number printed for each command. Vite requires Node.js 20.19 or higher.

✔️ I see version 20.19 or higher

Node.js is ready to go. Move on to the next substep.

ⓧ I see an older version

Your installed version of Node.js is too old for Vite. You need version 20.19 or higher.

  • Go to https://nodejs.org and download the LTS version.
  • Run the installer. It will replace your older version with the latest LTS.
  • Close and reopen your terminal, then run node --version again to confirm it shows 20.19 or higher.

ⓧ Command not found

Node.js is not installed on your machine yet.

  • Go to https://nodejs.org and click the LTS download button.
  • Run the installer and accept all default options.
  • Close and reopen your terminal after the install finishes.
  • Run node --version again. You should now see a version number.
Install VS Code

Visual Studio Code is a free code editor that works well with React and TypeScript. It provides syntax highlighting, autocomplete, and a built-in terminal.

  • Open https://code.visualstudio.com in your browser.
  • Click the download button for your operating system.
  • Run the installer and accept the default options.
  • Once installed, open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Visual Studio Code, and pressing Enter.

VS Code not showing up in search?

On macOS, you may need to drag the VS Code app from your Downloads folder into your Applications folder first. On Windows, make sure you checked the option to add VS Code to your PATH during installation.

Help me get VS Code to open from my search bar.

Scaffold the React project with Vite

Vite is a build tool that generates a ready-to-use React project in seconds. It sets up the folder structure, configuration files, and a development server that automatically refreshes when you save changes.

Why Vite instead of Create React App?

Create React App was the old standard for starting React projects, but it was officially deprecated in February 2025. React's own documentation now recommends Vite as the primary tool for building React apps from scratch.

Vite is faster at starting up, refreshes changes instantly, and produces smaller production builds.

  • In VS Code, open the built-in terminal by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your React project by running this command:
npm create vite@latest task-tracker -- --template react-ts

What does this command do?

npm create vite@latest runs Vite's project generator. task-tracker is the name of your project folder. --template react-ts tells Vite to use the React + TypeScript template.

  • Move into your new project folder by running:
cd task-tracker
  • Install the project dependencies by running:
npm install

You should see a progress bar followed by a message showing how many packages were added.

Seeing permission errors or EACCES?

On macOS, try prefixing the command with sudo. On Windows, close your terminal and reopen it by right-clicking and choosing Run as administrator.

Help me fix npm permission errors on my machine.

  • Open the task-tracker folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the task-tracker folder, and click Open.

Now let's customize two files so the project is ready for your Task Tracker app.

  • In the Explorer sidebar on the left, click index.html to open it.
  • Find this line:
<title>Vite + React + TS</title>
  • Replace it with:
<title>Task Tracker</title>
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Now click src/main.tsx in the Explorer sidebar to open it.
  • Replace the entire file contents with this code:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

What changed here?

You removed the import './index.css' line because you'll write your own CSS from scratch in a later step. You also simplified the App import path.

  • Save src/main.tsx.

Finally, start the development server to see your app in the browser.

  • Open the VS Code terminal again with Ctrl+` and start the dev server by running:
npm run dev
  • Open your browser and go to http://localhost:5173.

You should see the default Vite + React page with a counter button. The browser tab title should say Task Tracker.

Page not loading at localhost:5173?

Make sure the terminal still shows the dev server running (you should see a local URL printed). If you closed the terminal or pressed Ctrl+C, run npm run dev again.

If port 5173 is already in use, Vite will pick the next available port and display it in the terminal output.

Help me get the Vite dev server running.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files.

ⓧ I'd like to double check the full code

Here is index.html in full:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Task Tracker</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Here is src/main.tsx in full:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Your development environment is ready and the project skeleton is in place. Next up, you'll replace the default content with your own Task Tracker components and state.

Build the App Component and Task State

Your development environment is ready and the default Vite app is running in your browser.

Now it's time to build the core of your task tracker. React apps work by storing data in state and rendering UI based on that state.

In this step, you'll define what a task looks like, create functions to manage your task list, and render a basic UI to verify everything connects.

In this step, get ready to:
  • Define the Task type and initial state.
  • Create handler functions for adding, toggling, and deleting tasks.
  • Render a placeholder UI that displays the task count.
Define the Task type and initial state

Every task in your app needs a consistent shape. TypeScript interfaces let you define that shape once, so the compiler catches mistakes whenever you create or update a task.

  • In VS Code, open src/App.tsx from the Explorer sidebar on the left.
  • Select all the existing code (press Cmd+A on macOS or Ctrl+A on Windows) and delete it.
  • Replace the file contents with this code:
import { useState } from 'react'
import './App.css'

// Define the shape of a single task
export interface Task {
  id: number
  text: string
  completed: boolean
}

function App() {
  // State: an array of tasks, starting empty
  const [tasks, setTasks] = useState<Task[]>([])

  return (
    <div>
      <h1>Task Tracker</h1>
    </div>
  )
}

export default App

What does this code do?

  • export interface Task defines the shape of a task object with three fields: a numeric id, a string of text, and a completed boolean.
  • useState<Task[]>([]) creates a state variable that holds an array of tasks, starting empty. React re-renders the component whenever this array changes.
  • The export keyword on the interface lets other files import the Task type later.
  • Save src/App.tsx.
  • Check your browser at http://localhost:5173. You should see a plain "Task Tracker" heading, replacing the default Vite content.

Don't see the heading?

  • Make sure you saved the file. Unsaved changes won't trigger a browser refresh.
  • Check the terminal where your dev server is running for any red error messages. A missing bracket or typo in the code will prevent it from compiling.
  • Confirm you edited src/App.tsx (not src/App.css or another file).

Help me debug why my App component isn't rendering.

Create handler functions

Your component has state, but no way to change it yet. You need three functions: one to add a task, one to toggle its completion status, and one to delete it.

  • In src/App.tsx, find the useState line:
  const [tasks, setTasks] = useState<Task[]>([])
  • Add the following three functions below that line, above the return statement:
  // Add a new task to the array
  function addTask(text: string) {
    const newTask: Task = {
      id: Date.now(),
      text,
      completed: false,
    }
    setTasks([...tasks, newTask])
  }

  // Flip the completed status of a task
  function toggleTask(id: number) {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    )
  }

  // Remove a task from the array
  function deleteTask(id: number) {
    setTasks(tasks.filter((task) => task.id !== id))
  }

What do these functions do?

  • addTask creates a new task object using Date.now() for a unique ID, then spreads the existing array and appends the new task.
  • toggleTask maps through every task. When it finds the matching ID, it returns a copy with completed flipped. All other tasks stay unchanged.
  • deleteTask filters the array, keeping every task except the one whose ID matches.
  • Save src/App.tsx.
  • You won't see a visible change in the browser yet. The next substep will use these functions in the UI.

Seeing a red error in the terminal?

  • Make sure each function is placed inside the App function body, between the useState line and the return statement.
  • Check that you haven't accidentally deleted the closing } of the App function or the export default App line at the bottom.

Help me fix the compile error in my App.tsx.

Render a placeholder UI

Your state and handler functions are in place. Now update the JSX to display the task count and list task texts, proving that state and rendering are connected.

  • In src/App.tsx, find the existing return block:
  return (
    <div>
      <h1>Task Tracker</h1>
    </div>
  )
  • Replace it with this updated return block:
  return (
    <div>
      <h1>Task Tracker</h1>
      <p>{tasks.length} task{tasks.length !== 1 ? 's' : ''} total</p>
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>{task.text}</li>
        ))}
      </ul>
    </div>
  )

What does this JSX do?

  • {tasks.length} displays the current number of tasks in the array.
  • The ternary expression adds an "s" for pluralization when the count is not 1.
  • tasks.map() iterates over the array and renders each task's text inside a <li> element.
  • key={task.id} gives React a stable identifier for each list item so it can efficiently update the DOM when tasks change.
  • Save src/App.tsx.
  • Check your browser. You should see the "Task Tracker" heading and "0 tasks total" below it.

Don't see the task count?

  • Make sure you replaced the entire return block, including the opening and closing parentheses.
  • Check that curly braces {} wrap the JavaScript expressions inside JSX. Without them, the code renders as plain text.

Help me debug why my task count isn't showing.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/App.tsx at the end of this step.

import { useState } from 'react'
import './App.css'

export interface Task {
  id: number
  text: string
  completed: boolean
}

function App() {
  const [tasks, setTasks] = useState<Task[]>([])

  function addTask(text: string) {
    const newTask: Task = {
      id: Date.now(),
      text,
      completed: false,
    }
    setTasks([...tasks, newTask])
  }

  function toggleTask(id: number) {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    )
  }

  function deleteTask(id: number) {
    setTasks(tasks.filter((task) => task.id !== id))
  }

  return (
    <div>
      <h1>Task Tracker</h1>
      <p>{tasks.length} task{tasks.length !== 1 ? 's' : ''} total</p>
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>{task.text}</li>
        ))}
      </ul>
    </div>
  )
}

export default App

Your app has a solid data foundation with a typed Task interface and three handler functions ready to go. Next up, you'll build a form component that lets users actually add new tasks to the list.

Create the TaskForm Component

Your App component has the logic to add, toggle, and delete tasks. But right now, there's no way for a user to actually type in a new task and submit it.

In this step, you'll create a separate TaskForm component with its own state for tracking text input. Then you'll connect it to App so that submitting the form adds a real task to your list.

In this step, get ready to:
  • Build a TaskForm component with a controlled text input and submit button.
  • Connect TaskForm to App by passing functions as props.
Build the TaskForm component

In React, each piece of your UI can live in its own file as a component. Your task form is responsible for one thing: collecting text from the user and sending it up to App.

  • In the Explorer sidebar in VS Code, right-click the src folder.
  • Select New File.
  • Name the file TaskForm.tsx and press Enter.
  • Add the full TaskForm component by pasting this code into the file:
import { useState } from 'react'

interface TaskFormProps {
  onAdd: (text: string) => void
}

function TaskForm({ onAdd }: TaskFormProps) {
  const [text, setText] = useState('')

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    if (text.trim() === '') return
    onAdd(text.trim())
    setText('')
  }

  return (
    <form className="task-form" onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Add a new task..."
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button type="submit">Add</button>
    </form>
  )
}

export default TaskForm

What does this code do?

  • TaskFormProps defines what this component expects from its parent: an onAdd function that accepts a string.
  • useState('') creates a controlled input. React tracks the input's value in state so it always stays in sync with what's displayed.
  • The <form> element wraps the input and button so that pressing Enter or clicking Add both trigger submission.
  • Save src/TaskForm.tsx.
  • You won't see a change in the browser yet. The component exists but nothing renders it. The next substep connects it to App.

Seeing a red underline in VS Code?

  • Make sure the file is named exactly TaskForm.tsx (capital T, capital F, .tsx extension).
  • Confirm the file is inside the src folder, not the project root.
  • Check that the first line is import { useState } from 'react'. A missing import causes TypeScript errors.

Why is my TaskForm file showing errors?

✔️ Awesome, I've got everything!

Great. Double check you've saved src/TaskForm.tsx before moving on.

ⓧ I'd like to double check the full code

import { useState } from 'react'

interface TaskFormProps {
  onAdd: (text: string) => void
}

function TaskForm({ onAdd }: TaskFormProps) {
  const [text, setText] = useState('')

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    if (text.trim() === '') return
    onAdd(text.trim())
    setText('')
  }

  return (
    <form className="task-form" onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Add a new task..."
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button type="submit">Add</button>
    </form>
  )
}

export default TaskForm
Handle form submission

The handleSubmit function in your TaskForm handles the entire submission flow. This pattern comes up in every React form you'll ever build.

How does handleSubmit work?

  • e.preventDefault() stops the browser from refreshing the page. Without it, a form submission reloads everything and your React state disappears.
  • if (text.trim() === '') return guards against empty submissions. Trimming removes leading and trailing spaces so a user can't submit a blank task.
  • onAdd(text.trim()) calls the function that the parent component (App) passed in as a prop. This is how data flows upward in React.
  • setText('') clears the input field so it's ready for the next task.

What are props?

Props are how React components talk to each other. A parent passes data or functions down to a child through props. Here, App will pass its addTask function to TaskForm as the onAdd prop.

The TaskFormProps interface acts as a contract. TypeScript will warn you if App forgets to pass onAdd or passes the wrong type.

Wire TaskForm into App

Your TaskForm component is ready, but App doesn't know it exists yet. You need to import it and render it inside App's JSX.

  • Switch back to src/App.tsx.
  • Find the import section at the top of the file. Add this import on the line below import './App.css':
import TaskForm from './TaskForm'
  • Scroll down to the return statement. Find this line:
<h1>Task Tracker</h1>
  • Add this line directly below it:
<TaskForm onAdd={addTask} />

What does this line do?

This renders the TaskForm component and passes App's addTask function as the onAdd prop. When the user submits the form, TaskForm calls onAdd which triggers addTask in App, adding the new task to state.

  • Save src/App.tsx.
  • Return to your browser at http://localhost:5173. You should see a text input field and an Add button below the heading.
  • Type a task name into the input and click Add. The task should appear in the list below and the input should clear itself.

Not seeing the input field?

  • Check that your import statement says import TaskForm from './TaskForm'. A missing ./ prefix means the bundler can't find the file.
  • Make sure <TaskForm onAdd={addTask} /> is inside the <div> wrapper in your return statement. JSX placed outside the root element won't render.
  • Check the terminal running your dev server for error messages. A typo in the import path usually shows a clear module-not-found error.

Why isn't my TaskForm rendering in the browser?

✔️ Awesome, I've got everything!

Great. Make sure both src/TaskForm.tsx and src/App.tsx are saved before moving on.

ⓧ I'd like to double check the full code

import { useState } from 'react'
import './App.css'
import TaskForm from './TaskForm'

export interface Task {
  id: number
  text: string
  completed: boolean
}

function App() {
  const [tasks, setTasks] = useState<Task[]>([])

  function addTask(text: string) {
    const newTask: Task = {
      id: Date.now(),
      text,
      completed: false,
    }
    setTasks([...tasks, newTask])
  }

  function toggleTask(id: number) {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    )
  }

  function deleteTask(id: number) {
    setTasks(tasks.filter((task) => task.id !== id))
  }

  return (
    <div>
      <h1>Task Tracker</h1>
      <TaskForm onAdd={addTask} />
      <p>{tasks.length} task{tasks.length !== 1 ? 's' : ''} total</p>
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>{task.text}</li>
        ))}
      </ul>
    </div>
  )
}

export default App

Your form is working and tasks appear in the list. Next up, you'll build dedicated TaskList and TaskItem components to give each task its own toggle and delete buttons.

Create the TaskList and TaskItem Components

Your App component has the logic to add, toggle, and delete tasks. Your TaskForm lets users type in new tasks. The missing piece is displaying those tasks in a way that lets users interact with them.

In this step, you'll build two new React components that work together. TaskItem renders a single task with action buttons. TaskList maps over your tasks array and renders a TaskItem for each one.

In this step, get ready to:
  • Build a TaskItem component that displays a task with toggle and delete buttons.
  • Build a TaskList component that renders TaskItem for each task in the array.
  • Wire both components into App so the full UI is functional.
Build the TaskItem component

Each task in your list needs its own visual representation. The TaskItem component is responsible for rendering a single task's text along with buttons to toggle its completion status and delete it.

  • In VS Code, create a new file called src/TaskItem.tsx.
  • Add the following code to build the TaskItem component:
import { Task } from './App'

// Define what props TaskItem expects
interface TaskItemProps {
  task: Task
  onToggle: (id: number) => void
  onDelete: (id: number) => void
}

function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
  return (
    <li className="task-item">
      <span
        className={`task-text ${task.completed ? 'completed' : ''}`}
        onClick={() => onToggle(task.id)}
      >
        {task.text}
      </span>
      <div className="task-actions">
        <button
          className="toggle-btn"
          onClick={() => onToggle(task.id)}
        >
          {task.completed ? '↩' : '✓'}
        </button>
        <button
          className="delete-btn"
          onClick={() => onDelete(task.id)}
        >
          ✕
        </button>
      </div>
    </li>
  )
}

export default TaskItem

What does this code do?

TaskItem receives three props: the task object itself, an onToggle callback, and an onDelete callback. It doesn't manage any state of its own.

  • The span displays the task text and gets a conditional completed className when the task is done.
  • The toggle button shows a checkmark (✓) when the task is active and a return arrow (↩) when it's completed.
  • The delete button (✕) calls onDelete with the task's ID to remove it from the list.
  • Save src/TaskItem.tsx.
  • You won't see a change in the browser yet. The next component (TaskList) will use TaskItem, and then you'll wire both into App.

Seeing a red underline on the import?

If VS Code shows an error on import { Task } from './App', make sure your src/App.tsx file exports the Task interface with the export keyword (export interface Task).

help me fix my TaskItem import error

Build the TaskList component

TaskList is the container component. It receives the full tasks array and renders a TaskItem for each entry. It also handles the empty state when there are no tasks to show.

  • Create a new file called src/TaskList.tsx.
  • Add the following code:
import { Task } from './App'
import TaskItem from './TaskItem'

// Define what props TaskList expects
interface TaskListProps {
  tasks: Task[]
  onToggle: (id: number) => void
  onDelete: (id: number) => void
}

function TaskList({ tasks, onToggle, onDelete }: TaskListProps) {
  // Show a message when there are no tasks
  if (tasks.length === 0) {
    return <p className="empty-message">No tasks yet. Add one above!</p>
  }

  return (
    <ul className="task-list">
      {tasks.map((task) => (
        <TaskItem
          key={task.id}
          task={task}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  )
}

export default TaskList

What does this code do?

  • The early return (if (tasks.length === 0)) handles the empty state with a friendly message.
  • The tasks.map() call loops over each task and renders a TaskItem for it.
  • The key={task.id} prop gives React a stable identifier for each list item. React uses keys to efficiently update only the items that changed instead of re-rendering the entire list.

Why does TaskList pass callbacks down to TaskItem?

TaskList doesn't own the tasks state. App does. When the user clicks a toggle or delete button inside TaskItem, the event needs to travel back up to App where the state lives. This pattern of passing callbacks from a parent through intermediate components is a core React data flow.

  • Save src/TaskList.tsx.
  • You still won't see a change in the browser. The next substep wires TaskList into App.

Red underline on the TaskItem import?

Check that you saved src/TaskItem.tsx in the previous substep. The import resolves against saved files on disk, not unsaved editor tabs.

help me debug my TaskList import

Assemble the components in App

Both new components are ready. Now you'll replace the placeholder list in App with the real TaskList component, connecting your state and handlers to the UI.

  • In src/App.tsx, find the import section at the top of the file:
import { useState } from 'react'
import './App.css'
import TaskForm from './TaskForm'
  • Add the TaskList import on a new line below the TaskForm import, so your imports look like this:
import { useState } from 'react'
import './App.css'
import TaskForm from './TaskForm'
import TaskList from './TaskList'
  • Now find the return block in your App function:
  return (
    <div>
      <h1>Task Tracker</h1>
      <TaskForm onAdd={addTask} />
      <p>{tasks.length} task{tasks.length !== 1 ? 's' : ''} total</p>
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>{task.text}</li>
        ))}
      </ul>
    </div>
  )
  • Replace the entire return block with this updated version:
  return (
    <div className="app">
      <h1>Task Tracker</h1>
      <TaskForm onAdd={addTask} />
      <TaskList tasks={tasks} onToggle={toggleTask} onDelete={deleteTask} />
      <p className="task-count">{tasks.length} task{tasks.length !== 1 ? 's' : ''} total</p>
    </div>
  )

What changed here?

  • The wrapper <div> now has className="app" so you can style the container in the next step.
  • The entire <ul> block is replaced by a single <TaskList> component that receives your state and handler functions as props.
  • The task count paragraph now has className="task-count" and is placed after the list for better visual flow.
  • Save src/App.tsx.
  • Switch to your browser at http://localhost:5173. You should see the text "No tasks yet. Add one above!" below the form.
  • Type a task name in the input and click Add. The task appears with a ✓ button and a ✕ button next to it.
  • Click the ✓ button to mark the task as completed. Click ✕ to delete it.

Buttons not working or tasks not appearing?

  • Check that you passed onToggle={toggleTask} and onDelete={deleteTask} to the TaskList component. Missing props mean the buttons have nothing to call.
  • If you see a blank page, open your browser's DevTools console (press F12) and look for error messages. A common cause is a typo in an import path.
  • Make sure all three files (App.tsx, TaskList.tsx, TaskItem.tsx) are saved.

help me debug my task tracker components

✔️ Awesome, I've got everything!

Great. Make sure all three files are saved before moving on.

ⓧ I'd like to double check the full code

Here's the complete code for all three files at this point.

import { useState } from 'react'
import './App.css'
import TaskForm from './TaskForm'
import TaskList from './TaskList'

export interface Task {
  id: number
  text: string
  completed: boolean
}

function App() {
  const [tasks, setTasks] = useState<Task[]>([])

  function addTask(text: string) {
    const newTask: Task = {
      id: Date.now(),
      text,
      completed: false,
    }
    setTasks([...tasks, newTask])
  }

  function toggleTask(id: number) {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    )
  }

  function deleteTask(id: number) {
    setTasks(tasks.filter((task) => task.id !== id))
  }

  return (
    <div className="app">
      <h1>Task Tracker</h1>
      <TaskForm onAdd={addTask} />
      <TaskList tasks={tasks} onToggle={toggleTask} onDelete={deleteTask} />
      <p className="task-count">{tasks.length} task{tasks.length !== 1 ? 's' : ''} total</p>
    </div>
  )
}

export default App
import { Task } from './App'
import TaskItem from './TaskItem'

interface TaskListProps {
  tasks: Task[]
  onToggle: (id: number) => void
  onDelete: (id: number) => void
}

function TaskList({ tasks, onToggle, onDelete }: TaskListProps) {
  if (tasks.length === 0) {
    return <p className="empty-message">No tasks yet. Add one above!</p>
  }

  return (
    <ul className="task-list">
      {tasks.map((task) => (
        <TaskItem
          key={task.id}
          task={task}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  )
}

export default TaskList
import { Task } from './App'

interface TaskItemProps {
  task: Task
  onToggle: (id: number) => void
  onDelete: (id: number) => void
}

function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
  return (
    <li className="task-item">
      <span
        className={`task-text ${task.completed ? 'completed' : ''}`}
        onClick={() => onToggle(task.id)}
      >
        {task.text}
      </span>
      <div className="task-actions">
        <button
          className="toggle-btn"
          onClick={() => onToggle(task.id)}
        >
          {task.completed ? '↩' : '✓'}
        </button>
        <button
          className="delete-btn"
          onClick={() => onDelete(task.id)}
        >
          ✕
        </button>
      </div>
    </li>
  )
}

export default TaskItem

Your component hierarchy is complete. App owns the state, TaskList organizes the list, and TaskItem handles each individual task. Next up, you'll add styling to make the app look polished and professional.

Style the App

Your Task Tracker is fully functional. You can add tasks, toggle them complete, and delete them. But it still looks like raw, unstyled HTML.

In this step, you'll replace the default Vite styles with a clean, custom design. You'll also verify that the conditional styling you built into TaskItem actually creates a visible strikethrough when tasks are completed.

In this step, get ready to:
  • Replace the default Vite CSS with custom task tracker styles.
  • Verify conditional styling for completed tasks.
  • Test adding, toggling, and deleting tasks with the finished UI.
Replace the default CSS

The src/App.css file still has the default Vite demo styles. You'll replace everything with styles that target the class names your components already use.

  • In VS Code, open src/App.css from the Explorer sidebar.
  • Select all the existing content with Cmd+A (macOS) or Ctrl+A (Windows) and delete it.
  • Paste the following styles for the app container, heading, and form:
/* Center the app and set a comfortable max width */
.app {
  max-width: 500px;
  margin: 2rem auto;
  padding: 2rem;
  font-family: system-ui, -apple-system, sans-serif;
}

/* Style the page heading */
h1 {
  text-align: center;
  color: #1a1a2e;
  margin-bottom: 1.5rem;
}

/* Layout the form as a horizontal row */
.task-form {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

/* Style the text input */
.task-form input {
  flex: 1;
  padding: 0.75rem 1rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 1rem;
  outline: none;
  transition: border-color 0.2s;
}

/* Highlight the input border when focused */
.task-form input:focus {
  border-color: #646cff;
}

/* Style the Add button */
.task-form button {
  padding: 0.75rem 1.5rem;
  background: #646cff;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  cursor: pointer;
  transition: background 0.2s;
}

/* Darken the button on hover */
.task-form button:hover {
  background: #535bf2;
}

What does this code do?

These styles handle the page layout and the input form.

  • .app centers the whole tracker on the page with a 500px max width.
  • .task-form uses flexbox to place the input and button side by side.
  • The transition property creates smooth color changes on focus and hover.
  • Add the following styles below what you just pasted for the task list, items, buttons, and utility messages:
/* Remove default list bullets */
.task-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* Style each task row */
.task-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1rem;
  margin-bottom: 0.5rem;
  background: #f8f9fa;
  border-radius: 8px;
  transition: background 0.2s;
}

/* Highlight the row on hover */
.task-item:hover {
  background: #e9ecef;
}

/* Make the task text clickable */
.task-text {
  flex: 1;
  cursor: pointer;
  user-select: none;
}

/* Strikethrough and dim completed tasks */
.task-text.completed {
  text-decoration: line-through;
  color: #999;
}

/* Layout the action buttons */
.task-actions {
  display: flex;
  gap: 0.25rem;
}

/* Shared styles for toggle and delete buttons */
.toggle-btn,
.delete-btn {
  width: 32px;
  height: 32px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.9rem;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s;
}

/* Green toggle button */
.toggle-btn {
  background: #d4edda;
  color: #155724;
}

.toggle-btn:hover {
  background: #c3e6cb;
}

/* Red delete button */
.delete-btn {
  background: #f8d7da;
  color: #721c24;
}

.delete-btn:hover {
  background: #f5c6cb;
}

/* Task counter at the bottom */
.task-count {
  text-align: center;
  color: #666;
  margin-top: 1rem;
  font-size: 0.9rem;
}

/* Message when no tasks exist */
.empty-message {
  text-align: center;
  color: #999;
  padding: 2rem;
  font-style: italic;
}

What does this code do?

These styles control how individual tasks display and how users interact with them.

  • .task-item uses flexbox to space the task text and action buttons apart in a row.
  • .task-text.completed applies the strikethrough and lighter color when a task is marked done.
  • .toggle-btn and .delete-btn use green and red backgrounds to visually signal their purpose.
  • Save src/App.css.
  • Switch to your browser. You should see the task tracker centered on the page with a styled input, a purple Add button, and the text "No tasks yet. Add one above!" in italic gray.

Still seeing the old Vite styles?

  • Make sure you deleted ALL the old CSS before pasting. If any old rules remain, they can conflict.
  • Check that you saved the file. Unsaved changes won't trigger Vite's hot reload.
  • Verify your dev server is still running in the terminal. If it stopped, run npm run dev again.

Help me debug why my CSS styles aren't showing up in the browser.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and the styling looks right in the browser.

ⓧ I'd like to double check the full code

.app {
  max-width: 500px;
  margin: 2rem auto;
  padding: 2rem;
  font-family: system-ui, -apple-system, sans-serif;
}

h1 {
  text-align: center;
  color: #1a1a2e;
  margin-bottom: 1.5rem;
}

.task-form {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.task-form input {
  flex: 1;
  padding: 0.75rem 1rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 1rem;
  outline: none;
  transition: border-color 0.2s;
}

.task-form input:focus {
  border-color: #646cff;
}

.task-form button {
  padding: 0.75rem 1.5rem;
  background: #646cff;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  cursor: pointer;
  transition: background 0.2s;
}

.task-form button:hover {
  background: #535bf2;
}

.task-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.task-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1rem;
  margin-bottom: 0.5rem;
  background: #f8f9fa;
  border-radius: 8px;
  transition: background 0.2s;
}

.task-item:hover {
  background: #e9ecef;
}

.task-text {
  flex: 1;
  cursor: pointer;
  user-select: none;
}

.task-text.completed {
  text-decoration: line-through;
  color: #999;
}

.task-actions {
  display: flex;
  gap: 0.25rem;
}

.toggle-btn,
.delete-btn {
  width: 32px;
  height: 32px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.9rem;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s;
}

.toggle-btn {
  background: #d4edda;
  color: #155724;
}

.toggle-btn:hover {
  background: #c3e6cb;
}

.delete-btn {
  background: #f8d7da;
  color: #721c24;
}

.delete-btn:hover {
  background: #f5c6cb;
}

.task-count {
  text-align: center;
  color: #666;
  margin-top: 1rem;
  font-size: 0.9rem;
}

.empty-message {
  text-align: center;
  color: #999;
  padding: 2rem;
  font-style: italic;
}
Verify conditional styling for completed tasks

Your TaskItem component already applies a dynamic class name based on whether a task is completed. The CSS you just wrote targets that class to create the visual effect.

How does conditional styling work here?

In TaskItem.tsx, the span uses a template literal for its className:

className={`task-text ${task.completed ? 'completed' : ''}`}

When task.completed is true, the element gets both task-text and completed classes. The CSS rule .task-text.completed then applies the strikethrough and lighter color.

  • In your browser, type a task name and click Add.
  • Click on the task text or the green checkmark button to toggle it complete.
  • You should see the task text turn gray with a line through it.

No strikethrough when you click?

  • Open your browser's DevTools with F12 and inspect the task text span. Check that the completed class appears when you click it.
  • If the class toggles but the style doesn't change, check that your CSS has .task-text.completed with no space between the two class names. A space changes the meaning entirely.

Help me debug why my completed tasks don't show a strikethrough.

Run a final test

Time to put the whole app through its paces. You'll test every interaction to confirm everything works together.

  • Add three or four tasks using the form.
  • Click a task's text to toggle it complete. Confirm the strikethrough appears.
  • Click the same task again (or the undo arrow) to toggle it back to active.
  • Click the red X button on a task to delete it.
  • Check that the task count at the bottom updates as you add and delete tasks.

Your app should feel responsive and snappy. Hover effects on the task rows and buttons confirm that the CSS transitions are working.

Something not responding to clicks?

  • If a button does nothing, check the browser console for errors. A typo in a prop name can silently break the callback.
  • If the task count doesn't update, verify that addTask and deleteTask in App.tsx both call setTasks correctly.

Help me figure out why my task tracker buttons aren't working.

Secret mission

Add Task Filtering

Your task tracker works great, but what happens when you have dozens of tasks? Add a filter bar that lets you toggle between All, Active, and Completed tasks so you can focus on what matters most.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The task-tracker project folder on your Desktop.
  • The Vite dev server running at http://localhost:5173.
  • Node.js installed on your system.
  • VS Code installed on your system.

Keep everything running

No action needed. Choose this if you're still actively building or want to continue experimenting with React.

  • Your dev server stays running at http://localhost:5173.
  • Your task-tracker folder remains on your Desktop, ready for edits.
  • Node.js and VS Code stay installed for future projects.

Pause - I'll come back to this later

Stop the running dev server to free up your terminal, but keep all files and tools in place.

  • Switch to the terminal where your dev server is running.
  • Press Ctrl+C to stop the Vite dev server.
  • Your task-tracker folder stays on your Desktop. When you're ready to resume, open a terminal in that folder and run npm run dev to start the server again.

Delete - I don't want to use this again

Remove all project files and optionally uninstall the tools. Follow these steps to start fresh.

  • Switch to the terminal where your dev server is running and press Ctrl+C to stop it.
  • Delete the project folder. On macOS/Linux, run:
rm -rf ~/Desktop/task-tracker

On Windows, run:

rmdir /s /q "$env:USERPROFILE\Desktop\task-tracker"
  • To uninstall Node.js on macOS, delete the Node binary and associated folders. On Windows, open Settings, select Apps, find Node.js, and click Uninstall.
  • To uninstall VS Code on macOS, drag the application from your Applications folder to the Trash. On Windows, open Settings, select Apps, find Visual Studio Code, and click Uninstall.

Keep Node.js and VS Code

Node.js and VS Code are general-purpose development tools you'll reuse in future projects. Only uninstall them if you're sure you won't need them again.

Nice Work!

Nice Work!

Nice work! You just built a fully interactive Task Tracker from scratch using React, Vite, and TypeScript.

You've learned how to:

  • Built a multi-component React application with clear separation of concerns, passing data and callbacks between components via props.
  • Managed interactive state with React's useState hook and handled user events like form submissions and button clicks.
  • Rendered dynamic lists with proper key usage and applied conditional styling based on component state.
  • Added filtering functionality to view tasks by their completion status.

Ready to quiz yourself?