Building a Task List Tracker with Vite and React

A Beginner's Guide to Creating a To-Do List Web Application

Introduction to Our Project

Welcome, future web developers! Today we're embarking on a journey to build something we all need in our daily lives: a task list tracker (also known as a to-do list). This seemingly simple application is actually a perfect learning tool because it involves many fundamental concepts in modern web development: component structure, state management, user interactions, and even basic CRUD operations (Create, Read, Update, Delete).

Think of a task list as the "Hello World" of interactive applications—simple enough to build in a few hours, but complex enough to teach important concepts that you'll use in much larger applications.

Prerequisites

Before we dive in, make sure you have the following installed:

What is Vite?

Vite (French for "quick", pronounced /vit/) is a build tool that significantly improves the development experience. Created by Evan You (the same developer behind Vue.js), Vite has quickly become popular for both Vue and React projects.

Why Use Vite?

Think of Vite as a sports car compared to the reliable but slower family sedan of traditional bundlers. It gets you from development to production faster and with less hassle.

Project Setup

Let's create our project using Vite's scaffolding tool. Open your terminal and run:

npm create vite@latest tasktracker -- --template react

This command creates a new directory called "tasktracker" with a basic React setup using Vite. Navigate into your new project folder and install the dependencies:

cd tasktracker
npm install

Now start the development server:

npm run dev

You should see output in your terminal with a local URL (typically http://localhost:5173). Open this URL in your browser to see the default Vite + React template.

Project Structure

Here's what your project structure looks like after creation:

tasktracker/
├── node_modules/
├── public/
│   └── vite.svg
├── src/
│   ├── assets/
│   │   └── react.svg
│   ├── App.css
│   ├── App.jsx
│   ├── index.css
│   └── main.jsx
├── .eslintrc.cjs
├── .gitignore
├── index.html
├── package.json
├── package-lock.json
└── vite.config.js

The key files we'll be working with are:

Think of this structure as the blueprint for a house. We have our foundation (index.html), the main entrance (main.jsx), and the living room (App.jsx) where we'll be spending most of our time building.

Cleaning Up the Template

Before we start building our task tracker, let's clean up the template to remove unnecessary code. Open src/App.jsx and replace its contents with:

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

function App() {
  return (
    <div className="app-container">
      <h1>Task Tracker</h1>
    </div>
  )
}

export default App

Next, let's simplify src/App.css:

.app-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

h1 {
  font-size: 2.5rem;
  margin-bottom: 2rem;
  color: #333;
}

Also update src/index.css with some basic reset styles:

:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;
  color: #213547;
  background-color: #f5f5f5;
}

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

body {
  min-height: 100vh;
}

Component Planning

Before diving into code, let's plan our component structure. A good React application is built with a clear component hierarchy.

For our task tracker, we'll need:

This structure follows the single responsibility principle: each component does one thing and does it well. Think of it like a restaurant kitchen: the head chef (App) oversees everything, while specialized chefs handle specific parts of the meal (TaskForm creates new items, TaskList organizes them, TaskItem presents each dish).

Creating Our Components

First, let's create a components folder to organize our code. In the src directory, create a new folder called components.

mkdir src/components

Task Form Component

Create a file called src/components/TaskForm.jsx:

import { useState } from 'react';

function TaskForm({ addTask }) {
  const [taskText, setTaskText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (taskText.trim() === '') return;
    
    addTask(taskText);
    setTaskText('');
  };

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

export default TaskForm;

Task Item Component

Create a file called src/components/TaskItem.jsx:

function TaskItem({ task, deleteTask, toggleComplete }) {
  return (
    <div className={`task-item ${task.completed ? 'completed' : ''}`}>
      <div className="task-info">
        <input 
          type="checkbox" 
          checked={task.completed}
          onChange={() => toggleComplete(task.id)}
        />
        <p>{task.text}</p>
      </div>
      <button onClick={() => deleteTask(task.id)}>Delete</button>
    </div>
  );
}

export default TaskItem;

Task List Component

Create a file called src/components/TaskList.jsx:

import TaskItem from './TaskItem';

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

  return (
    <div className="task-list">
      {tasks.map(task => (
        <TaskItem 
          key={task.id} 
          task={task} 
          deleteTask={deleteTask} 
          toggleComplete={toggleComplete}
        />
      ))}
    </div>
  );
}

export default TaskList;

Integrating Components

Now let's update our App.jsx to use these components:

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

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

  // Generate a unique ID for new tasks
  const generateId = () => {
    return Date.now().toString();
  };

  // Add a new task
  const addTask = (text) => {
    const newTask = {
      id: generateId(),
      text,
      completed: false
    };
    
    setTasks([...tasks, newTask]);
  };

  // Delete a task
  const deleteTask = (id) => {
    setTasks(tasks.filter(task => task.id !== id));
  };

  // Toggle task completion status
  const toggleComplete = (id) => {
    setTasks(tasks.map(task => 
      task.id === id ? { ...task, completed: !task.completed } : task
    ));
  };

  return (
    <div className="app-container">
      <h1>Task Tracker</h1>
      <TaskForm addTask={addTask} />
      <TaskList 
        tasks={tasks} 
        deleteTask={deleteTask} 
        toggleComplete={toggleComplete}
      />
    </div>
  )
}

export default App

Adding Styles

Let's enhance the look of our application by updating src/App.css:

.app-container {
  max-width: 500px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
  background-color: white;
  border-radius: 10px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  margin-top: 50px;
}

h1 {
  font-size: 2.5rem;
  margin-bottom: 2rem;
  color: #333;
}

.task-form {
  display: flex;
  margin-bottom: 2rem;
}

.task-form input {
  flex-grow: 1;
  padding: 0.8rem;
  border: 1px solid #ddd;
  border-radius: 4px 0 0 4px;
  font-size: 1rem;
}

.task-form button {
  padding: 0.8rem 1.2rem;
  background-color: #4caf50;
  color: white;
  border: none;
  border-radius: 0 4px 4px 0;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.3s;
}

.task-form button:hover {
  background-color: #45a049;
}

.task-list {
  text-align: left;
}

.task-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
  margin-bottom: 0.5rem;
  background-color: #f9f9f9;
  border-radius: 4px;
  transition: background-color 0.3s;
}

.task-item:hover {
  background-color: #f0f0f0;
}

.task-info {
  display: flex;
  align-items: center;
  gap: 10px;
}

.task-item p {
  margin: 0;
  font-size: 1.1rem;
}

.completed p {
  text-decoration: line-through;
  color: #aaa;
}

.task-item button {
  padding: 0.5rem 1rem;
  background-color: #f44336;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.task-item button:hover {
  background-color: #d32f2f;
}

.empty-message {
  text-align: center;
  color: #757575;
  font-style: italic;
  margin-top: 2rem;
}

Persisting Data with Local Storage

Our app works great, but when we refresh the page, all our tasks disappear! Let's fix that by using local storage to save our tasks.

Update App.jsx to use local storage:

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

function App() {
  // Initialize state from local storage or empty array
  const [tasks, setTasks] = useState(() => {
    const savedTasks = localStorage.getItem('tasks');
    if (savedTasks) {
      return JSON.parse(savedTasks);
    }
    return [];
  });

  // Save tasks to local storage whenever they change
  useEffect(() => {
    localStorage.setItem('tasks', JSON.stringify(tasks));
  }, [tasks]);

  // Generate a unique ID for new tasks
  const generateId = () => {
    return Date.now().toString();
  };

  // Add a new task
  const addTask = (text) => {
    const newTask = {
      id: generateId(),
      text,
      completed: false
    };
    
    setTasks([...tasks, newTask]);
  };

  // Delete a task
  const deleteTask = (id) => {
    setTasks(tasks.filter(task => task.id !== id));
  };

  // Toggle task completion status
  const toggleComplete = (id) => {
    setTasks(tasks.map(task => 
      task.id === id ? { ...task, completed: !task.completed } : task
    ));
  };

  return (
    <div className="app-container">
      <h1>Task Tracker</h1>
      <TaskForm addTask={addTask} />
      <TaskList 
        tasks={tasks} 
        deleteTask={deleteTask} 
        toggleComplete={toggleComplete} 
      />
    </div>
  )
}

export default App

With these changes, our tasks will persist even when the browser is refreshed.

Additional Features

Let's add a few more features to enhance our app:

Task Counter

Add a counter to show how many tasks remain. Create a new component called src/components/TaskStats.jsx:

function TaskStats({ tasks }) {
  const totalTasks = tasks.length;
  const completedTasks = tasks.filter(task => task.completed).length;
  const remainingTasks = totalTasks - completedTasks;
  
  return (
    <div className="task-stats">
      <p>
        {remainingTasks} task{remainingTasks !== 1 ? 's' : ''} remaining
        {totalTasks > 0 && ` (${Math.round((completedTasks / totalTasks) * 100)}% completed)`}
      </p>
    </div>
  );
}

export default TaskStats;

Now update App.jsx to include this component:

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

function App() {
  // ... existing code ...

  return (
    <div className="app-container">
      <h1>Task Tracker</h1>
      <TaskForm addTask={addTask} />
      <TaskStats tasks={tasks} />
      <TaskList 
        tasks={tasks} 
        deleteTask={deleteTask} 
        toggleComplete={toggleComplete} 
      />
    </div>
  )
}

export default App

Add styles for the stats component in App.css:

.task-stats {
  margin: 1rem 0;
  color: #666;
  font-size: 0.9rem;
}

Clear Completed Button

Let's add a button to clear all completed tasks. Create a new component called src/components/TaskActions.jsx:

function TaskActions({ clearCompleted, hasCompletedTasks }) {
  return (
    <div className="task-actions">
      {hasCompletedTasks && (
        <button 
          className="clear-btn" 
          onClick={clearCompleted}
        >
          Clear Completed
        </button>
      )}
    </div>
  );
}

export default TaskActions;

Update App.jsx again:

import { useState, useEffect } from 'react'
import './App.css'
import TaskForm from './components/TaskForm'
import TaskList from './components/TaskList'
import TaskStats from './components/TaskStats'
import TaskActions from './components/TaskActions'

function App() {
  // ... existing code ...
  
  // Clear all completed tasks
  const clearCompleted = () => {
    setTasks(tasks.filter(task => !task.completed));
  };

  // Check if there are any completed tasks
  const hasCompletedTasks = tasks.some(task => task.completed);

  return (
    <div className="app-container">
      <h1>Task Tracker</h1>
      <TaskForm addTask={addTask} />
      <div className="task-header">
        <TaskStats tasks={tasks} />
        <TaskActions 
          clearCompleted={clearCompleted} 
          hasCompletedTasks={hasCompletedTasks} 
        />
      </div>
      <TaskList 
        tasks={tasks} 
        deleteTask={deleteTask} 
        toggleComplete={toggleComplete} 
      />
    </div>
  )
}

export default App

Add styles for the new components:

.task-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 1rem 0;
}

.clear-btn {
  padding: 0.5rem 1rem;
  background-color: #f0f0f0;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.8rem;
  transition: background-color 0.3s;
}

.clear-btn:hover {
  background-color: #e0e0e0;
}

Building for Production

Our task tracker is now complete! Let's build it for production.

Run the following command in your terminal:

npm run build

This will create a dist folder with optimized files ready for deployment. To preview the production build locally, you can use:

npm run preview

To deploy your app, you can upload the contents of the dist folder to any static hosting service like Netlify, Vercel, GitHub Pages, or Amazon S3.

Understanding the Code

Component Hierarchy

Our application follows a clear component hierarchy:

State Management

We're using React's useState hook to manage our application state. The main state is the tasks array, which contains objects with the following structure:

{
  id: "1647857322553",  // Unique identifier generated from timestamp
  text: "Buy groceries", // The task description
  completed: false       // Whether the task is completed
}

Think of state in React like a database that automatically updates your UI whenever it changes. When we add, delete, or modify tasks, React efficiently re-renders only the parts of the UI that need to change.

Props and Data Flow

Data flows down from parent components to children through props. Functions that modify state are also passed down as props, allowing child components to trigger state changes in the parent.

This one-way data flow is like a corporate hierarchy: the CEO (App component) sets the company strategy and delegates specific responsibilities to managers (child components), who can report back with requests for changes but don't make high-level decisions on their own.

Local Storage

We've used the browser's localStorage API to persist our tasks between page refreshes. The useEffect hook ensures our tasks are saved whenever they change.

Think of localStorage as a simple notebook where your browser can write information that persists even when the page is closed. It's perfect for small amounts of data like our task list.

Real-World Applications

Business Context

Task tracking applications like the one we've built are fundamental to productivity tools used in businesses worldwide. Products like Asana, Trello, and Todoist are sophisticated versions of the same concept.

The core CRUD operations (Create, Read, Update, Delete) we've implemented are the foundation of most business applications, from customer relationship management (CRM) systems to enterprise resource planning (ERP) software.

Learning Path Forward

Now that you've built a basic task tracker, consider these enhancements to practice more advanced concepts:

Conclusion

Congratulations! You've built a fully functional task tracker application using Vite and React. Through this project, you've learned:

This project serves as an excellent foundation for more complex React applications. The concepts you've learned—components, state, props, hooks—are the building blocks of modern web development and will serve you well as you continue your journey as a developer.

Remember, the best way to learn is by doing. Try adding some of the suggested enhancements, or create a completely different application using the same principles. Happy coding!

Final Folder Structure Reference

Here's the final folder structure of our project for reference:

tasktracker/
├── node_modules/
├── public/
│   └── favicon.png
├── src/
│   ├── components/
│   │   ├── TaskActions.jsx
│   │   ├── TaskForm.jsx
│   │   ├── TaskItem.jsx
│   │   ├── TaskList.jsx
│   │   └── TaskStats.jsx
│   ├── App.css
│   ├── App.jsx
│   ├── index.css
│   └── main.jsx
├── .eslintrc.cjs
├── .gitignore
├── index.html
├── package.json
├── package-lock.json
└── vite.config.js

Additional Resources

To deepen your understanding of the concepts covered in this tutorial, here are some valuable resources:

Remember that the web development ecosystem is constantly evolving. Stay curious, keep building, and don't be afraid to experiment with new technologies and approaches!