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:
- Node.js (version 14 or higher)
- npm (comes with Node.js) or yarn
- A code editor (VS Code recommended)
- Basic understanding of HTML, CSS, and JavaScript
- Familiarity with ES6 syntax (arrow functions, destructuring, etc.)
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?
- Blazing Fast Development Server: Unlike traditional bundlers like Webpack that bundle your entire application before serving, Vite uses native ES modules to serve your code, making the development server start almost instantly and updates happen in milliseconds.
- Hot Module Replacement (HMR): Changes to your code are reflected in the browser without a full page refresh, preserving component state.
- Optimized for Production: When building for production, Vite uses Rollup to create highly optimized bundles.
- Out-of-the-box Support: TypeScript, JSX, CSS preprocessing, and more work with minimal configuration.
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:
src/main.jsx: The entry point of our applicationsrc/App.jsx: The main component of our applicationindex.html: The HTML template for our application
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:
- App: The main container component
- TaskForm: For adding new tasks
- TaskList: Container for all tasks
- TaskItem: Individual task display and controls
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:
- App: The main component that manages state and contains all other components
- TaskForm: Handles user input for creating new tasks
- TaskList: Displays all tasks and handles conditional rendering when empty
- TaskItem: Renders individual tasks with their controls
- TaskStats: Shows statistics about task completion
- TaskActions: Provides additional actions like clearing completed tasks
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:
- Task Categories/Labels: Add the ability to categorize tasks by project or priority
- Due Dates: Implement date picking and sorting by due date
- User Accounts: Add authentication and store tasks in a backend database
- Drag and Drop: Allow users to reorder tasks using libraries like react-beautiful-dnd
- Responsive Design: Optimize the UI for mobile devices
- Dark Mode: Implement a theme toggle for light/dark mode
Conclusion
Congratulations! You've built a fully functional task tracker application using Vite and React. Through this project, you've learned:
- Setting up a project with Vite
- Creating and structuring React components
- Managing state with useState
- Passing data and functions through props
- Persisting data with localStorage and useEffect
- Handling user input and form submissions
- Styling a React application
- Building and deploying a production-ready app
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:
- Vite Documentation - Comprehensive guide to all Vite features
- React Documentation - Official React documentation
- React Learn - Interactive tutorials and guides
- localStorage API - MDN documentation on browser storage
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!