Why a Todo App is the Perfect Starting Point
The todo list is the "Hello World" of React applications, but building one properly teaches fundamental concepts that scale to production applications. Modern React development with hooks, proper state management, and performance considerations from the start.
This guide walks through building a production-ready todo app that demonstrates React best practices. Every pattern you learn here transfers directly to building complex web applications for our web development team.
What you'll learn:
- Component architecture and composition patterns
- State management with the useState hook
- CRUD operations (create, read, update, delete)
- Accessibility implementation (ARIA attributes)
- Modern CSS for React components
- When to scale with additional libraries
Everything you need to build a professional todo application
Project Setup
Initialize a React project with Vite for optimal developer experience and fast builds.
Component Structure
Learn how to organize components for maintainability and reusability.
State Management
Master useState hook and proper state update patterns for React applications.
CRUD Operations
Implement create, read, update, and delete functionality for todos.
Accessibility
Build inclusive apps with ARIA attributes and semantic HTML from day one.
Modern Styling
Apply CSS patterns that scale with your React application.
Setting Up Your React Project
Modern React development starts with modern tooling. Vite provides a lightning-fast development experience with instant server start and Hot Module Replacement (HMR) that makes development smooth and efficient.
Project Initialization
npm create vite@latest todo-app -- --template react
cd todo-app
npm install
npm run dev
Why Vite?
Vite offers significant advantages over older tooling:
- Instant server start - No bundling during development
- Lightning-fast HMR - Changes reflect immediately
- Optimized builds - Production builds are highly optimized
- Modern defaults - ES modules, TypeScript support, and more
Recommended Project Structure
src/
├── components/
│ ├── TodoApp.jsx # Main container with state
│ ├── TodoForm.jsx # Form for adding new todos
│ ├── TodoList.jsx # Renders the todo list
│ ├── TodoItem.jsx # Individual todo with checkbox and actions
│ └── TodoFilters.jsx # Filter buttons (All/Active/Completed)
├── hooks/
│ └── useTodos.js # Custom hook for todo logic
├── App.jsx
└── main.jsx
Following this structured approach ensures your codebase remains maintainable as complexity grows, a principle our professional web development services apply to every project we build.
1# Create a new React project with Vite2npm create vite@latest my-todo-app -- --template react3 4# Navigate to the project5cd my-todo-app6 7# Install dependencies8npm install9 10# Start the development server11npm run dev12 13# When ready for production14npm run buildBuilding the Todo App Structure
A well-structured todo app demonstrates how React components should be organized in production applications. The component hierarchy reflects data flow and makes the application easy to maintain and extend.
Component Hierarchy
TodoApp (main container, holds todos state)
├── TodoForm (input for new todos)
├── TodoFilters (filter buttons: All/Active/Completed)
└── TodoList (renders filtered todos)
└── TodoItem (individual todo with checkbox and actions)
The Initial TodoApp Component
function TodoApp() {
return (
<div className="todoapp stack-large">
<h1>TodoMatic</h1>
<form>
<h2 className="label-wrapper">
<label htmlFor="new-todo-input" className="label__lg">
What needs to be done?
</label>
</h2>
<input
type="text"
id="new-todo-input"
className="input input__lg"
name="text"
autoComplete="off"
/>
<button type="submit" className="btn btn__primary btn__lg">
Add
</button>
</form>
</div>
);
}
export default TodoApp;
This foundational structure, based on MDN's comprehensive React tutorial, establishes the component hierarchy and data flow that will scale as features grow.
Accessibility Considerations from the Start
Building accessible applications isn't an afterthought--it should be part of your initial component design. React makes it straightforward to implement proper accessibility patterns.
Key Accessibility Features
<button
type="button"
className="btn toggle-btn"
aria-pressed="true"
>
<span className="visually-hidden">Show </span>
<span>all</span>
<span className="visually-hidden"> tasks</span>
</button>
Accessibility explanations:
aria-pressedindicates the toggle button state for screen readersvisually-hiddentext provides context to assistive technology without cluttering the visual design- Semantic HTML (
<button>,<label>,<ul>) improves both accessibility and SEO htmlForon labels ensures form fields are properly associated
Visually Hidden CSS Pattern
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
This CSS ensures screen readers can access the context text while keeping it invisible to sighted users, following the accessibility patterns documented by MDN Web Docs.
Managing State with React Hooks
The useState hook is the foundation of React state management. Understanding how to structure and update state properly is essential for building reliable applications. Whether you're building a simple todo app or a complex business application, these state management principles apply universally across our web development projects.
Using useState
import { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build a todo app', completed: false },
{ id: 3, text: 'Deploy to production', completed: false }
]);
const [filter, setFilter] = useState('all');
// ... event handlers
}
State Structure Best Practices
Do this:
// Normalized data with unique IDs
const [todos, setTodos] = useState([
{ id: 'todo-1', text: 'Task 1', completed: false },
{ id: 'todo-2', text: 'Task 2', completed: true }
]);
// Filter state separate from data
const [filter, setFilter] = useState('all');
Avoid this:
// Redundant state that must be kept in sync
const [completedTodos, setCompletedTodos] = useState([]);
const [activeTodos, setActiveTodos] = useState([]);
// Derived state should be calculated, not stored
const allTodos = [...completedTodos, ...activeTodos];
These patterns align with React's official guidance on state structure.
Proper State Updates
// Adding a new todo - use spread operator
const addTodo = (text) => {
const newTodo = {
id: crypto.randomUUID(), // or Date.now()
text,
completed: false
};
setTodos([...todos, newTodo]);
};
// Toggling completion - map and preserve other items
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
// Deleting a todo - filter out the matching item
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
Implementing CRUD Operations
A complete todo application needs to handle all CRUD operations: Create, Read, Update, and Delete. Each operation has specific patterns in React.
Creating New Todos
function TodoForm({ onAddTodo }) {
const [inputValue, setInputValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (inputValue.trim()) {
onAddTodo(inputValue.trim());
setInputValue(''); // Clear input after adding
}
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="new-todo-input">What needs to be done?</label>
<input
type="text"
id="new-todo-input"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a new task..."
aria-label="Add a new todo item"
/>
<button type="submit">Add</button>
</form>
);
}
Reading and Displaying Todos
function TodoList({ todos, onToggle, onDelete }) {
if (todos.length === 0) {
return <p className="empty-state">No todos yet. Add one above!</p>;
}
return (
<ul role="list" aria-label="Todo list">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li className={`todo ${todo.completed ? 'completed' : ''}`}>
<div className="todo-content">
<input
type="checkbox"
id={`todo-${todo.id}`}
checked={todo.completed}
onChange={() => onToggle(todo.id)}
aria-label={`Mark ${todo.text} as ${todo.completed ? 'incomplete' : 'complete'}`}
/>
<label htmlFor={`todo-${todo.id}`}>
{todo.text}
</label>
</div>
<div className="todo-actions">
<button
type="button"
onClick={() => onDelete(todo.id)}
className="btn btn__danger"
aria-label={`Delete ${todo.text}`}
>
Delete
</button>
</div>
</li>
);
}
The list rendering pattern follows React's official documentation on rendering lists, which emphasizes the critical importance of stable keys for efficient updates.
Editing Todos (Inline)
function TodoItem({ todo, onToggle, onDelete, onEdit }) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleEdit = () => {
if (editText.trim()) {
onEdit(todo.id, editText.trim());
setIsEditing(false);
}
};
if (isEditing) {
return (
<li className="todo editing">
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onBlur={handleEdit}
onKeyDown={(e) => e.key === 'Enter' && handleEdit()}
autoFocus
aria-label="Edit todo"
/>
<button type="button" onClick={handleEdit}>Save</button>
</li>
);
}
return (
<li className={`todo ${todo.completed ? 'completed' : ''}`}>
{/* Display mode markup */}
</li>
);
}
Filtering Todos
function TodoFilters({ currentFilter, onFilterChange }) {
const filters = [
{ value: 'all', label: 'All' },
{ value: 'active', label: 'Active' },
{ value: 'completed', label: 'Completed' }
];
return (
<div className="filters btn-group" role="group" aria-label="Filter todos">
{filters.map(filter => (
<button
key={filter.value}
type="button"
className={`btn toggle-btn ${currentFilter === filter.value ? 'active' : ''}`}
aria-pressed={currentFilter === filter.value}
onClick={() => onFilterChange(filter.value)}
>
<span className="visually-hidden">Show </span>
<span>{filter.label}</span>
<span className="visually-hidden"> tasks</span>
</button>
))}
</div>
);
}
// Filter logic
const getFilteredTodos = (todos, filter) => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
};
Styling the Todo App
Modern React applications deserve modern CSS. The todo app demonstrates how to structure styles that are maintainable, responsive, and accessible.
Core CSS Architecture
/* Layout utilities */
.stack > * + * {
margin-top: 1rem;
}
.stack-large > * + * {
margin-top: 2rem;
}
/* Main container */
.todoapp {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
/* Todo item styling */
.todo {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: white;
transition: box-shadow 0.2s ease;
}
.todo:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.todo.completed label {
text-decoration: line-through;
color: #888;
}
/* Form inputs */
.input__lg {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1.125rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
transition: border-color 0.2s ease;
}
.input__lg:focus {
outline: none;
border-color: #007bff;
}
/* Button variants */
.btn {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s ease;
}
.btn:hover {
background: #f5f5f5;
}
.btn__primary {
background: #007bff;
color: white;
border-color: #007bff;
}
.btn__primary:hover {
background: #0056b3;
}
.btn__danger {
background: #dc3545;
color: white;
border-color: #dc3545;
}
.btn__danger:hover {
background: #c82333;
}
.btn__lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
/* Filter buttons */
.filters.btn-group {
display: flex;
gap: 0.5rem;
}
.toggle-btn[aria-pressed="true"] {
background: #007bff;
color: white;
border-color: #007bff;
}
/* Accessibility utility */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
These styling patterns, derived from MDN's React tutorial approach, prioritize readability and maintainability.
Responsive Design
@media (max-width: 600px) {
.todoapp {
padding: 1rem;
}
.btn__lg {
width: 100%;
margin-top: 0.5rem;
}
.input__lg {
font-size: 1rem;
}
.todo {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.todo-actions {
width: 100%;
display: flex;
justify-content: flex-end;
}
.filters.btn-group {
flex-wrap: wrap;
}
}
Connecting to Modern Data Patterns
While local state works for simple todo apps, production applications often need server-side persistence and more sophisticated state management. For complex applications requiring AI-powered features or automation, our AI automation services can help scale your React applications further.
When to Consider TanStack Query
For todo apps that need:
- Server-side persistence - Save todos to a database
- Cache management - Automatic caching and refetching
- Optimistic updates - Immediate UI updates while saving
- Background sync - Keep data fresh without user action
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useTodos() {
const queryClient = useQueryClient();
const { data: todos, isLoading } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('/api/todos');
return response.json();
}
});
const addTodoMutation = useMutation({
mutationFn: async (newTodo) => {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
});
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries(['todos']);
}
});
return { todos, isLoading, addTodoMutation };
}
TanStack Query has become the industry standard for server state, as analyzed in recent state management discussions.
URL State for Persistence
Store filter state in the URL for shareable links:
import { useSearchParams } from 'react-router';
function TodoFilters({ currentFilter, onFilterChange }) {
const [searchParams, setSearchParams] = useSearchParams();
const handleFilterChange = (filter) => {
setSearchParams({ filter });
onFilterChange(filter);
};
// Get initial filter from URL
const filterFromUrl = searchParams.get('filter') || 'all';
return (
<div className="filters">
{['all', 'active', 'completed'].map(filter => (
<button
key={filter}
onClick={() => handleFilterChange(filter)}
className={filterFromUrl === filter ? 'active' : ''}
>
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</button>
))}
</div>
);
}
Custom Hook for Reusable Logic
Extract todo logic into a custom hook:
function useTodos() {
const [todos, setTodos] = useState([
{ id: '1', text: 'Learn React', completed: false }
]);
const [filter, setFilter] = useState('all');
const addTodo = (text) => {
const newTodo = {
id: crypto.randomUUID(),
text,
completed: false
};
setTodos([...todos, newTodo]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const editTodo = (id, text) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text } : todo
));
};
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return {
todos: filteredTodos,
allTodos: todos,
filter,
setFilter,
addTodo,
toggleTodo,
deleteTodo,
editTodo
};
}
Key principles for production-ready React applications
Single Source of Truth
Keep state in one place, derive what you need. Avoid redundant state that must be synchronized.
Immutable Updates
Never mutate state directly. Use spread operator, map(), and filter() for predictable updates.
Component Composition
Build small, focused components that do one thing well. Compose them for complex UIs.
Accessibility First
Implement ARIA attributes and semantic HTML from the start. Inclusive apps are better apps.
Modern Tooling
Use Vite for fast development. Leverage React hooks for cleaner, more intuitive code.
Scale Thoughtfully
Know when local state is enough and when to introduce libraries like TanStack Query.
Frequently Asked Questions
Sources
- MDN Web Docs: Beginning our React ToDo app - Comprehensive tutorial covering todo app structure, styling, and accessibility patterns
- React.dev: Managing State - Official React documentation on state management principles and useState patterns
- Developer Way: React State Management in 2025 - Modern perspective on state management covering TanStack Query and Context API patterns