Build a Task Tracker App with React

Build an interactive task tracker app using React components, props, and state.

Introduction

30 Second Summary

Everyone has a mental list of things to get done. Whether it is work tasks, shopping items, or weekend plans, tracking what needs doing is something we all handle daily.

In this project, you will build a task tracker app using React. You will create interactive components that let users add tasks, mark them as complete, and delete them.

What You'll Build

A task tracker app powered by multiple React components that manage state and communicate through props.

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

  • A Vite-powered React project running locally in your browser.
  • Multiple components that pass data using props and manage interactivity with the useState hook.
  • A complete add, toggle, and delete workflow proving you can build interactive UIs from scratch.
  • Secret Mission: Add a filter bar that lets users switch between All, Active, and Completed tasks.

Are there any prerequisites?

You should have basic HTML, CSS, and JavaScript knowledge (variables, functions, arrays). No prior React experience is needed.

Before We Start

Before We Start...

Set Up Your Development Environment

Before you can build anything with React, you need a few tools on your machine. React apps are built and run using Node.js, a JavaScript runtime that powers the development tooling behind the scenes.

In this step, you will install Node.js, set up VS Code as your code editor, and scaffold a brand-new React project using Vite. By the end, you will have a live app running in your browser.

In this step, get ready to:
  • Install Node.js and verify it works.
  • Set up VS Code with a React extension.
  • Scaffold and run a React project with Vite.
Install Node.js

Node.js lets you run JavaScript outside the browser. Vite (the tool you will use to build your React app) requires Node.js version 20.19 or higher.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
  • Check if Node.js is already installed by running this command:
node --version
  • Also check that npm (Node's package manager) is available by running:
npm --version

✔️ I see version 20.19 or higher

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

ⓧ I see an older version

You have Node.js installed, but Vite requires version 20.19 or higher. You need to upgrade.

  • Go to https://nodejs.org and download the LTS version.
  • Run the installer and follow the prompts.
  • Close and reopen your terminal, then run node --version again to confirm you see v20.19 or higher.

Why close and reopen the terminal?

Your terminal loads its path settings when it first opens. After installing a new version of Node.js, you need to restart the terminal so it picks up the updated version.

ⓧ Command not found

Node.js is not installed on your machine yet. Let's fix that.

  • Go to https://nodejs.org and download the LTS version (this also installs npm automatically).
  • Run the installer and accept the default settings.
  • Close and reopen your terminal.
  • Run node --version and npm --version to confirm both show version numbers.
Install VS Code and the React extension

VS Code is a free code editor that works well with React projects. You will also install an extension that provides helpful shortcuts for writing React components.

  • If you do not already have VS Code, download it from https://code.visualstudio.com and install it.
  • Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), typing VS Code, and pressing Enter.
  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type ES7+ React/Redux/React-Native snippets into the search bar.
  • Click Install on the extension by dsznajder.

What does this extension do?

This extension gives you keyboard shortcuts for common React patterns. For example, typing rafce and pressing Tab generates a full React component skeleton. It saves time once you start writing components in the next steps.

Scaffold your React project with Vite

Vite is a build tool that creates a ready-to-run React project in seconds. It handles all the behind-the-scenes configuration so you can focus on writing components.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Scaffold your React project, install its dependencies, and start the dev server by running these commands:
  • If npm asks for permission to install the create-vite package, enter y to continue.
npm create vite@latest task-tracker -- --template react
cd task-tracker
npm install
npm run dev

What do these commands do?

  • npm create vite@latest task-tracker -- --template react creates a new folder called task-tracker with all the React starter files inside.
  • cd task-tracker moves your terminal into the new project folder.
  • npm install downloads all the packages React and Vite need to run.
  • npm run dev starts a local development server so you can preview your app in the browser.
  • Open http://localhost:5173 in your browser.
  • You should see the default Vite + React welcome page with the Vite and React logos.

Don't see the welcome page?

  • Make sure you ran all four commands in order. If you skipped npm install, the dev server will not start.
  • Check your terminal for error messages. If you see a port conflict, another app may already be using port 5173.
  • Make sure you are inside the task-tracker folder before running npm run dev.

Help me troubleshoot my Vite dev server not starting.

Now let's prepare the project files for the task tracker you will build next.

  • Open the task-tracker folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Select the task-tracker folder on your Desktop, then click Open.
  • In the Explorer sidebar, open index.html.
  • Find the <title> line:
<title>task-tracker</title>
  • Replace task-tracker with Task Tracker, so the line now looks like this:
<title>Task Tracker</title>
  • Save index.html.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

Here is the full index.html for comparison:

<!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.jsx"></script>
  </body>
</html>
  • Now open src/main.jsx in the Explorer sidebar.
  • Find and delete the line that says:
import './index.css'
  • Save src/main.jsx.

Why remove this import?

The default Vite template ships with a global index.css stylesheet. You will replace all styling with your own App.css in the next step, so this import is no longer needed.

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file.

ⓧ I'd like to double check the full code

Here is the full src/main.jsx for comparison:

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

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
  • Check your browser at http://localhost:5173. The page still loads (the tab title now says Task Tracker).

Your development environment is ready and your React app is live in the browser. Next up, you will replace the default welcome page with your own App component that manages task state.

Build the App Component with State

Your development environment is ready, and the default Vite welcome page is running in your browser. Time to replace it with something useful.

In this step, you'll build the core App component that manages your task tracker's data. This includes the state that holds your tasks and the functions that add, toggle, and delete them.

In this step, get ready to:
  • Build the App component with state management using useState.
  • Define handler functions for adding, toggling, and deleting tasks.
  • Style the app container with custom CSS.
Replace App.jsx with your task tracker component

React apps are built from components. Your App component is the top-level parent that owns the task data and passes it down to child components.

  • In VS Code, open src/App.jsx from the Explorer sidebar.
  • Select all the existing code and replace it with the following:
import { useState } from 'react'
import TaskForm from './components/TaskForm'
import TaskList from './components/TaskList'
import './App.css'

function App() {
  // Initialize tasks as an empty array
  const [tasks, setTasks] = useState([])

  // Add a new task to the array
  const addTask = (text) => {
    const newTask = {
      id: Date.now(),
      text,
      completed: false,
    }
    setTasks([...tasks, newTask])
  }

  // Toggle a task's completed status
  const toggleTask = (id) => {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    )
  }

  // Remove a task from the array
  const deleteTask = (id) => {
    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} />
    </div>
  )
}

export default App

What does this code do?

  • useState([]) creates an empty array to store your tasks. React re-renders the component whenever this array changes.
  • addTask creates a new task object with a unique ID (using Date.now()), the text you typed, and completed: false. It adds this to the tasks array using the spread operator.
  • toggleTask finds a task by ID and flips its completed value between true and false.
  • deleteTask filters out the task with the matching ID, removing it from the array.
  • The return statement renders JSX with a heading and two child components. TaskForm receives addTask as a prop called onAdd. TaskList receives the tasks array and both handler functions as props.
  • Save src/App.jsx.
  • Check your browser at http://localhost:5173. You should see an error overlay mentioning a failed import. This is expected because TaskForm and TaskList don't exist yet.

Seeing a syntax error instead?

  • Make sure you replaced ALL the existing code in App.jsx. Leftover code from the Vite template causes conflicts.
  • Check that every opening brace { has a matching closing brace }. The toggleTask function has nested parentheses that are easy to miss.
  • help me fix a syntax error in my App.jsx

✔️ 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

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

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

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

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

  const deleteTask = (id) => {
    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} />
    </div>
  )
}

export default App
Style the app with custom CSS

The styles you add now will apply to all the components you build in the next steps. This includes the card container, form layout, task rows, and buttons.

  • Open src/App.css in VS Code.
  • Select all the existing code and replace it with the following:
/* Reset default browser styles */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Center the app on the page */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f5f5;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  padding-top: 60px;
}

/* Card-style container */
.app {
  width: 100%;
  max-width: 500px;
  padding: 30px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  height: fit-content;
}

.app h1 {
  text-align: center;
  margin-bottom: 24px;
  color: #333;
}

/* Task form layout */
.task-form {
  display: flex;
  gap: 8px;
  margin-bottom: 24px;
}

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

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

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

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

/* Task list styles */
.task-list {
  list-style: none;
}

.task {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border-bottom: 1px solid #f0f0f0;
}

.task:last-child {
  border-bottom: none;
}

.task input[type='checkbox'] {
  width: 18px;
  height: 18px;
  cursor: pointer;
}

.task-text {
  flex: 1;
  font-size: 16px;
  color: #333;
}

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

/* Delete button */
.delete-btn {
  padding: 6px 12px;
  background-color: #ff4d4d;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.delete-btn:hover {
  background-color: #e60000;
}

/* Empty state message */
.empty-message {
  text-align: center;
  color: #999;
  padding: 20px;
  font-style: italic;
}

What do these styles do?

  • The * reset removes default browser margins and padding so your layout starts clean.
  • The .app class creates a white card centered on a light gray background with rounded corners and a subtle shadow.
  • The .task-form styles arrange the input and button side by side using flexbox.
  • The .task-text.completed class adds a strikethrough effect for finished tasks.
  • Many of these classes target components you haven't built yet. They'll take effect automatically once those components exist.
  • Save src/App.css.
  • You won't see a visual change yet because the browser is still showing the import error. The styles will apply once you build the missing components in the next steps.

CSS file not saving?

  • Make sure you're editing src/App.css and not a different CSS file. The Vite template may also include an index.css file in the same folder.
  • Check for a white dot on the file tab in VS Code. This means the file has unsaved changes. Press Cmd+S (macOS) or Ctrl+S (Windows) to save.
  • help me with saving my CSS file in VS Code

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f5f5;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  padding-top: 60px;
}

.app {
  width: 100%;
  max-width: 500px;
  padding: 30px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  height: fit-content;
}

.app h1 {
  text-align: center;
  margin-bottom: 24px;
  color: #333;
}

.task-form {
  display: flex;
  gap: 8px;
  margin-bottom: 24px;
}

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

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

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

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

.task-list {
  list-style: none;
}

.task {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border-bottom: 1px solid #f0f0f0;
}

.task:last-child {
  border-bottom: none;
}

.task input[type='checkbox'] {
  width: 18px;
  height: 18px;
  cursor: pointer;
}

.task-text {
  flex: 1;
  font-size: 16px;
  color: #333;
}

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

.delete-btn {
  padding: 6px 12px;
  background-color: #ff4d4d;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.delete-btn:hover {
  background-color: #e60000;
}

.empty-message {
  text-align: center;
  color: #999;
  padding: 20px;
  font-style: italic;
}

Your App component is ready with full state management. Next up, you'll build the TaskForm component so you can actually add tasks to your tracker.

Build the TaskForm Component

Your App component is set up with state management and handler functions. Right now, your browser shows an error because App.jsx imports two components that don't exist yet.

In this step, you'll build the first of those components: TaskForm. This is your first child component, and it manages its own local state independently from the App's task list state.

In this step, get ready to:
  • Create a components folder and the TaskForm.jsx file.
  • Build a form component with its own local state and submission handling.
  • Verify the form renders with the styling already in App.css.
Create the TaskForm component

The TaskForm component needs to track what the user types into the input field. It keeps that value in its own local state, separate from the tasks array in App.

  • In VS Code, right-click the src folder in the Explorer sidebar.
  • Click New Folder and name it components.
  • Right-click the new components folder and click New File.
  • Name the file TaskForm.jsx.
  • Copy the following code into TaskForm.jsx:
import { useState } from 'react'

function TaskForm({ onAdd }) {
  // Local state to track what the user types
  const [text, setText] = useState('')

  const handleSubmit = (e) => {
    // Prevent the page from refreshing on form submit
    e.preventDefault()
    // Don't add empty tasks
    if (text.trim() === '') return
    // Pass the trimmed text up to App's addTask function
    onAdd(text.trim())
    // Clear the input field
    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?

  • { onAdd } destructures the prop that App passes down. This is the addTask function from App.
  • useState('') creates a text variable that starts as an empty string. This tracks the input field's current value.
  • handleSubmit prevents the default page refresh, blocks empty submissions, sends the trimmed text to App via onAdd, then resets the input.
  • The value={text} and onChange pair keeps the input in sync with React state. This is called a controlled component.
  • Save TaskForm.jsx.
  • Check your browser. The error message should now only mention TaskList, not TaskForm. This confirms Vite found your new component.

Still seeing TaskForm in the error?

  • Confirm your file is saved. VS Code shows a dot on unsaved file tabs.
  • Check the file path. The file must be at src/components/TaskForm.jsx (not directly inside src/).
  • Make sure the filename matches exactly: TaskForm.jsx with a capital T and capital F.

Help me debug my TaskForm import error

Understand how props connect TaskForm to App

Your TaskForm component demonstrates a core React pattern: a child component communicates with its parent through props.

How does data flow here?

In App.jsx, you wrote <TaskForm onAdd={addTask} />. This passes App's addTask function down to TaskForm as a prop called onAdd.

When the user submits the form, TaskForm calls onAdd(text.trim()). This triggers addTask in App, which updates the tasks state. Data flows down through props, and actions flow back up through callback functions.

What about the form styling?

The .task-form CSS class is already in your App.css from the previous step. It uses display: flex to place the input and button side by side. Once the TaskList error is resolved in the next step, you'll see this styling in action.

✔️ 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's the complete TaskForm.jsx file:

import { useState } from 'react'

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

  const handleSubmit = (e) => {
    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

Your first child component is ready to accept user input. Next up, you'll build the TaskList and Task components so your tasks actually render on screen.

Build the TaskList and Task Components

Your App component has the logic to manage tasks, and TaskForm handles user input. Now you need the components that actually display the tasks on screen.

In this step, you will build two more components that work together. TaskList receives the full array of tasks and renders a Task component for each one. Each Task displays a single item with controls to mark it complete or delete it.

In this step, get ready to:
  • Create a TaskList component that maps over your tasks array.
  • Create a Task component with toggle and delete controls.
  • Verify the complete app works end to end.
Create the TaskList component

The TaskList component is responsible for rendering the full list of tasks. It receives the tasks array from App and maps over it, creating one Task component for each item.

  • Create a new file called TaskList.jsx inside the src/components folder.
  • Add the following code to TaskList.jsx:
import Task from './Task'

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

  // Render each task as a Task component
  return (
    <ul className="task-list">
      {tasks.map((task) => (
        <Task
          key={task.id}
          task={task}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  )
}

export default TaskList

What does this code do?

The if (tasks.length === 0) check handles the empty state. Instead of showing a blank area, users see a helpful message.

The tasks.map() call loops through every task object and returns a <Task> component for each one. The key={task.id} prop tells React how to track each item efficiently when the list changes.

Why does React need a key prop?

When you render a list, React needs a unique identifier for each item so it knows which ones changed, were added, or were removed. Without keys, React would re-render the entire list on every change instead of just the affected items.

  • Save TaskList.jsx.

Your browser may show an error because TaskList imports a Task component that does not exist yet. That is expected. You will create it next.

Seeing a different error?

If your error mentions something other than the missing Task import, check that the file is saved inside src/components/ and that the filename is exactly TaskList.jsx (capital T, capital L).

help me debug my TaskList component error

Create the Task component

Each Task component renders a single task item. It displays a checkbox, the task text, and a delete button. When the task is completed, the text gets a strikethrough style.

  • Create a new file called Task.jsx inside the src/components folder.
  • Add the following code to Task.jsx:
function Task({ task, onToggle, onDelete }) {
  return (
    <li className="task">
      <input
        type="checkbox"
        checked={task.completed}
        onChange={() => onToggle(task.id)}
      />
      <span className={task.completed ? 'task-text completed' : 'task-text'}>
        {task.text}
      </span>
      <button className="delete-btn" onClick={() => onDelete(task.id)}>
        Delete
      </button>
    </li>
  )
}

export default Task

What does this code do?

The checkbox input uses checked={task.completed} to reflect the current state. When clicked, onChange calls onToggle(task.id) which triggers the toggleTask function in App.

The className on the span uses a ternary expression. If task.completed is true, it applies both task-text and completed classes. The completed class adds a strikethrough style.

  • Save Task.jsx.
  • Check your browser at http://localhost:5173. You should see the task form and an italic message that says "No tasks yet. Add one above!" below it.

Don't see the empty message?

Make sure both TaskList.jsx and Task.jsx are saved. The error from the missing Task import should now be resolved.

If you see a blank page, open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and check for error messages.

help me fix my Task Tracker app not rendering

Verify your task styling works

The CSS for your task items is already in App.css from Step 2. Now that the components exist, those styles will kick in as soon as you add a task.

  • Type a task name into the input field and click Add.
  • You should see your task appear as a row with a checkbox on the left, the task text in the middle, and a red Delete button on the right.
  • Click the checkbox to toggle the task. The text should get a strikethrough and turn grey.
  • Click Delete to remove the task. The empty message should reappear.

How do props connect everything?

Data flows in one direction through your component tree. App holds the tasks state and passes it down to TaskList. TaskList passes each individual task to a Task component. When a user clicks a checkbox or delete button, the event travels back up through the handler functions (onToggle, onDelete) that were passed down as props.

This pattern keeps your state in one place while letting child components trigger updates.

Checkbox not toggling or delete not working?

Make sure the onChange on the checkbox uses an arrow function: () => onToggle(task.id). Without the arrow function, it would call onToggle immediately on render instead of on click.

The same applies to the delete button's onClick. It must be () => onDelete(task.id) not onDelete(task.id).

help me debug why my task toggle or delete isn't working

✔️ Awesome, I've got everything!

Great. Make sure both files are saved in src/components/.

ⓧ I'd like to double check the full code

Here is the complete code for both new files.

import Task from './Task'

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

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

export default TaskList
function Task({ task, onToggle, onDelete }) {
  return (
    <li className="task">
      <input
        type="checkbox"
        checked={task.completed}
        onChange={() => onToggle(task.id)}
      />
      <span className={task.completed ? 'task-text completed' : 'task-text'}>
        {task.text}
      </span>
      <button className="delete-btn" onClick={() => onDelete(task.id)}>
        Delete
      </button>
    </li>
  )
}

export default Task

Your task tracker is fully functional. You can add tasks, mark them complete, and delete them. Next up, you will test the full app and create a production build.

Test Your Complete App

You have built all four components of your task tracker. The App, TaskForm, TaskList, and Task components are all wired together with props and state.

Now it is time to put your app through its paces. You will test every feature in the browser, then create a production build to confirm the app is ready for the real world.

In this step, get ready to:
  • Test adding, toggling, and deleting tasks in the browser.
  • Create a production build and preview it locally.
Add tasks and verify they appear

Your dev server should still be running from the previous step. If it is not, run npm run dev in your terminal to start it again.

  • Open your browser to http://localhost:5173.
  • Click the text input field that says Add a new task... and type Buy groceries.
  • Click the Add button.
  • Add two more tasks: Walk the dog and Read a book.

Each task should appear in the list below the form as soon as you click Add. The input field clears itself after each submission.

Tasks not appearing?

Check that your App.jsx passes onAdd={addTask} as a prop to TaskForm. Without this prop, the form has no way to add tasks to the list.

Also confirm that TaskForm.jsx calls onAdd(text.trim()) inside handleSubmit. If it calls onAdd() without passing the text, tasks get added with no visible content.

help me debug why my tasks are not appearing in the list

Test toggling and deleting tasks

With three tasks in your list, you can now test the toggle and delete features.

  • Click the checkbox next to Buy groceries.
  • The text should get a strikethrough and turn grey. This confirms the toggleTask function and the conditional task-text completed class are working.
  • Click the same checkbox again to un-complete it. The strikethrough should disappear.
  • Click the Delete button next to Walk the dog. The task should vanish from the list immediately.
  • Delete the remaining tasks one by one. Once all tasks are removed, you should see the message No tasks yet. Add one above! displayed in italic grey text.

What is happening behind the scenes?

Every time you click a checkbox or delete button, React updates the tasks state array in App. This triggers a re-render, and React efficiently updates only the parts of the DOM that changed.

This is the core loop of React: state changes trigger re-renders, and re-renders update what the user sees. You never have to manually manipulate the DOM yourself.

Strikethrough not showing?

Make sure your Task.jsx uses the exact className logic: task.completed ? 'task-text completed' : 'task-text'. The CSS rule .task-text.completed applies the strikethrough style.

help me figure out why my strikethrough is not appearing when I check a task

Create a production build

Your app works perfectly in development mode. A production build bundles and optimizes your code so it loads as fast as possible for real users.

  • Stop the dev server by pressing Ctrl+C in your terminal.
  • Create the production build by running:
npm run build

You should see output showing the bundled files and their sizes. Vite compresses your code into a dist/ folder that is ready to deploy.

  • Preview the production build locally by running:
npm run preview
  • Open http://localhost:4173 in your browser.
  • Add a task, toggle it, and delete it. Everything should work exactly as it did in development mode.

What is the difference between dev and preview?

The npm run dev command serves your raw source files with hot-reload for fast development. The npm run preview command serves the optimized bundle from dist/ exactly as a hosting provider would.

If preview works, your app is ready to deploy anywhere that serves static files.

Build command failing?

Check for any red error messages in the terminal output. The most common cause is a typo in one of your component files (a missing closing tag or an unmatched bracket).

Make sure you stopped the dev server with Ctrl+C before running the build command.

help me fix my production build error

  • Stop the preview server with Ctrl+C when you are done testing.

Your task tracker is fully built and production-ready. Next up, you can take on the Secret Mission and add task filtering to level up your app even further.

Secret mission

Add Task Filtering

Your task tracker works great, but what happens when you have 50 tasks and need to see only the ones you haven't finished? In this secret mission, add a filter bar that lets users switch between All, Active, and Completed tasks.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your files to continue experimenting, or remove them entirely.

Resources you used:

  • The task-tracker project folder (source files and node_modules).
  • Node.js (installed in Step 1).
  • VS Code (installed in Step 1).

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the app with more features.

  • Your task-tracker folder stays on your Desktop.
  • Node.js and VS Code remain installed and ready for your next project.
  • To restart the dev server any time, open your terminal, navigate to the task-tracker folder, and run npm run dev.

Pause - I'll come back to this later

Stop any running processes to free up memory. Your project files stay on disk for when you return.

  • If your Vite dev server is still running, press Ctrl+C in the terminal to stop it.
  • Close VS Code.
  • Your task-tracker folder remains on your Desktop whenever you want to pick up again.

Delete - I don't want to use this again

Remove all project files and optionally uninstall the tools you set up. This cannot be undone.

  • If the Vite dev server is still running, press Ctrl+C in the terminal to stop it.
  • Delete the project folder by running the command for your operating system.

macOS

rm -rf ~/Desktop/task-tracker

Windows

rmdir /s /q %USERPROFILE%\Desktop\task-tracker
  • To uninstall Node.js on macOS, open Finder, go to Applications, and look for the Node.js uninstaller. On Windows, open Settings, select Apps, find Node.js in the list, and click Uninstall.
  • To uninstall VS Code on macOS, drag Visual Studio Code 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 for future projects

If you plan to do more web development, Node.js and VS Code are essential tools you will use again. Only uninstall them if you are sure you will not need them.

Nice Work!

Nice Work!

You just built a complete React app from scratch. Your task tracker lets you add tasks, mark them as complete, and delete them, all powered by reusable components and state management.

Here's what you learned:

  • Built a multi-component React application using Vite as your build tool.
  • Managed interactive state with the useState hook and passed data between components through props.
  • Handled user events (form submissions, button clicks) and used conditional rendering and list rendering to build an interactive UI.
  • Secret Mission: Added a filter bar to display tasks by status (All, Active, or Completed).

Ready to quiz yourself?