Build a simple TODO application using React and TypeScript. This project will help you practice component typing, state management, and event handling in a real application.
📚 Quick Reference: TypeScript + React Cheatsheet
- Create functional components with proper TypeScript typing
- Manage state with
useStateand proper type annotations - Handle form events with typed event handlers
- Practice component composition and prop passing
- Debug TypeScript errors in a real application
- Define a Todo type - Create an interface for todo items
- Add new todos - Form to input and add todos to the list
- Toggle completion - Click to mark todos as complete/incomplete
- Display todo list - Render all todos with their status
- Type all components - Proper TypeScript types for props, state, and handlers
- Filter todos - Show all, completed, or active todos
- Delete todos - Remove todos from the list
- Edit todos - Modify existing todo text
- Extract TodoItem component - Create reusable component
- Add input validation - Prevent empty todos
src/
├── components/
│ ├── TodoItem.tsx # Individual todo component
│ ├── TodoList.tsx # List of todos
│ └── AddTodoForm.tsx # Form to add new todos
├── types.ts # TypeScript type definitions
└── App.tsx # Main application component
-
Clone the repository and switch to main branch:
git clone <repository-url> git checkout main
-
Start the development server:
npm start
-
Check for TypeScript errors:
npx tsc --noEmit
Create a Todo interface in types.ts:
export interface Todo {
id: number;
text: string;
completed: boolean;
}In App.tsx, add state for managing todos:
const [todos, setTodos] = useState<Todo[]>([]);Implement a function to add new todos:
const addTodo = (text: string) => {
// Add implementation
};Implement a function to toggle todo completion:
const toggleTodo = (id: number) => {
// Add implementation
};Add some sample todos to test the application:
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: "Learn TypeScript", completed: false },
{ id: 2, text: "Build React components", completed: true },
{ id: 3, text: "Practice with hooks", completed: false }
]);Create a component to display individual todos:
interface TodoItemProps {
todo: Todo;
}
const TodoItem: React.FC<TodoItemProps> = ({ todo }) => {
return (
// Your implementation, consider using
// <div>
// <p>Todo Text Goes Here</p>
//</div>
);
};Add these styles to your App.css and use them to style the div an p tag
/* TodoItem Card Styles */
.todo-item {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 16px;
margin: 10px 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.todo-item:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.todo-text {
font-size: 16px;
color: #495057;
margin: 0;
}Display the first todo item in your App component.
Create a component to render all todos:
interface TodoListProps {
todos: Todo[];
}
const TodoList: React.FC<TodoListProps> = ({ todos }) => {
return (
<div>
// Iterate over todos and render TodoItems for each
</div>
);
};Display the TodoList in your app
Add a button to each TodoItem that calls the onToggle function:
Set the text of the button depending on the completion state to "Mark as Uncompleted" or "Mark as Completed"
<button onClick={() => call the toggle function here}>
Set the button text here
</button>Add these button styles to your App.css:
.todo-button {
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease;
margin-left: 12px;
}
.todo-button:hover {
background-color: #0056b3;
}
.todo-button:active {
background-color: #004085;
transform: translateY(1px);
}Modify TodoItem to modify the style and add a checkmark when completed using the styles below
Add these completion status styles to your App.css:
.todo-item.completed {
background: #e8f5e8;
border-color: #c3e6c3;
opacity: 0.8;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #6c757d;
}Build a form component for adding new todos:
interface AddTodoFormProps {
onAdd: (text: string) => void;
}
const AddTodoForm: React.FC<AddTodoFormProps> = ({ onAdd }) => {
const [inputValue, setInputValue] = useState<string>('');
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Do something with the input to add the new todo
};
const handleInputChange = (...) => {
// Handle input change here
}
return (
<form className="add-todo-form" onSubmit={/* What do we do here? */}>
<div className="form-group">
// Use an <input> and <button> with styles below
</div>
</form>
);
};Add these form styles to your App.css:
/* AddTodoForm Styles */
.add-todo-form {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border: 1px solid #e9ecef;
margin-bottom: 20px;
}
.form-group {
display: flex;
gap: 10px;
max-width: 500px;
margin: 0 auto;
}
.todo-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.2s;
}
.todo-input:focus {
outline: none;
border-color: #007bff;
}
.add-todo-btn {
padding: 12px 24px;
background-color: #28a745;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.add-todo-btn:hover {
background-color: #218838;
}
.add-todo-btn:active {
background-color: #1e7e34;
transform: translateY(1px);
}interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
}
const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggle }) => {
// component implementation
};const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// handle form submission
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// handle input change
};const [inputValue, setInputValue] = useState<string>('');
const [todos, setTodos] = useState<Todo[]>([]);{todos.length === 0 ? (
<p>No todos yet. Add one above!</p>
) : (
<TodoList todos={todos} onToggle={toggleTodo} />
)}const completedTodos = todos.filter(todo => todo.completed);
const activeTodos = todos.filter(todo => !todo.completed);-
TypeScript Compilation:
npx tsc --noEmit
Should show no errors.
-
Functionality Testing:
- Add a new todo
- Toggle todo completion
- Verify todos persist during the session
-
Type Safety:
- Try passing wrong prop types
- Verify TypeScript catches the errors
"Property 'X' does not exist on type 'Y'"
- Check that you've defined the property in your interface
- Verify the property name spelling
"Argument of type 'X' is not assignable to parameter of type 'Y'"
- Check the types of your function parameters
- Ensure you're passing the correct data types
"Object is possibly 'undefined'"
- Add optional chaining (
?.) or null checks - Provide default values where appropriate
- Use your editor's TypeScript integration
- Run
npx tsc --noEmitfrequently - Ask your group members for help
- Check the solution branch for reference
Your TODO app is complete when:
- ✅ All TypeScript errors are resolved
- ✅ You can add new todos
- ✅ You can toggle todo completion
- ✅ All components have proper type annotations
- ✅ The app runs without runtime errors
After completing the basic TODO app:
- Try the stretch goals
- Add localStorage persistence (homework)
- Explore the solution branch for reference
- Build your own TypeScript + React project