Project 2 > Enhanced Frontend
Now that you have a working app, let’s add the features that make it functional: displaying todos, creating new ones, and deleting them.
1. Build the READ Feature (Display Todos)
We’ll create a component that fetches and displays todos from the backend.
1.1. Create the Todos Component
Create ui/src/Todos.jsx:
import React, { useState, useEffect } from 'react';
export default function Todos({ API_URL }) {
const [todoList, setTodoList] = useState([]);
async function fetchTodos() {
const response = await fetch(`${API_URL}/todos`);
const data = await response.json();
setTodoList(data);
}
useEffect(() => {
fetchTodos();
}, []);
return (
<div className="todos-container">
<ul>
{todoList.map((todo) => (
<li key={todo.id}>
<div>
{todo.completed ? (
<span>Completed</span>
) : (
<span>In Progress</span>
)}{' '}
{todo.title}
<br />
{todo.description}
</div>
</li>
))}
</ul>
</div>
);
}
What this does:
- useState - Creates state to store the list of todos
- fetchTodos() - Makes a GET request to
/todosendpoint - useEffect - Runs
fetchTodos()when component first loads - map() - Loops through todos and creates HTML for each one
- Conditional rendering - Shows “Completed” or “In Progress”
Key concepts:
- State -
todoListstores data that can change - useEffect - Runs code when component mounts (empty array
[]means “run once”) - async/await - Handles asynchronous operations (API calls)
- fetch API - Browser function to make HTTP requests
1.2. Add Todos to App
Update ui/src/App.jsx:
import React from 'react';
import './globals.css';
import Todos from './Todos'; // Add this
const API_URL = 'http://localhost:8000'; // Add this
export default function App() {
return (
<>
<header>
<h1>TODO List</h1>
</header>
<main>
<Todos API_URL={API_URL} /> {/* Add this */}
</main>
</>
);
}
What this does:
- Imports the Todos component
- Defines the backend URL
- Passes
API_URLas a prop to Todos
Key concepts:
- Props - Data passed from parent (App) to child (Todos)
- Props are read-only - Child can’t change props directly
1.3. Add Styling for Todos
Add to ui/src/globals.css:
.todos-container {
max-width: 600px;
margin: 30px auto;
padding: 20px;
border: dashed 3px hotpink;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 10px;
background-color: #fff;
border-radius: 5px;
line-height: 1.4rem;
}
span {
font-weight: bold;
color: black;
background-color: hotpink;
padding: 2px 5px;
border-radius: 5px;
}
Understanding what just happened:
- When the page loads,
useEffectruns - It calls
fetchTodos()which makes a GET request - The backend returns JSON data
setTodoList(data)updates the state- React re-renders, showing the todos
Before you move on
- Verify your
Todos.jsxcomponent is created and imported inApp.jsx.- Test it now: Make sure your Docker containers are running (
docker-compose up -d), then visit http://localhost:5173. You should see any existing todos displayed in the list. If you don’t have any todos yet, that’s okay - you’ll create some in the next section.
2. Build the CREATE Feature (Add New Todos)
Now we’ll add a form to create new todos.
2.1. Create the CreateTodo Component
Create ui/src/CreateTodo.jsx:
import React, { useState } from 'react';
export default function CreateTodo({ API_URL }) {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
async function handleSubmit(e) {
e.preventDefault();
const response = await fetch(`${API_URL}/todos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description, completed: false }),
});
if (response.ok) {
setTitle('');
setDescription('');
}
}
return (
<form className="form-container" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<input
type="text"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<button type="submit">Create TODO</button>
</form>
);
}
What this does:
- useState - Tracks form input values (
titleanddescription) - handleSubmit() - Handles form submission
e.preventDefault()- Stops page from refreshing- Makes POST request to create a new todo
- Clears form if successful
- Controlled inputs - Input values are controlled by React state
Key concepts:
- Controlled components - Input value comes from state, not DOM
- onChange - Updates state when user types
- onSubmit - Handles form submission
- JSON.stringify() - Converts JavaScript object to JSON string
2.2. Add CreateTodo to App
Update ui/src/App.jsx:
import React from 'react';
import './globals.css';
import Todos from './Todos';
import CreateTodo from './CreateTodo'; // Add this
const API_URL = 'http://localhost:8000';
export default function App() {
return (
<>
<header>
<h1>TODO List</h1>
</header>
<main>
<Todos API_URL={API_URL} />
<CreateTodo API_URL={API_URL} /> {/* Add this */}
</main>
</>
);
}
2.3. Add Form Styling
Add to ui/src/globals.css:
.form-container {
max-width: 600px;
margin: 30px auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
display: flex;
flex-direction: column;
gap: 10px;
}
.form-container input {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}
.form-container button {
padding: 10px 20px;
background-color: hotpink;
color: black;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
.form-container button:hover {
background-color: #ff69b4;
}
Understanding what just happened:
- User fills out form and clicks submit
handleSubmitmakes a POST request- Backend creates the todo in the database
- But the Todos component doesn’t know to refresh!
Before you move on
- Verify your
CreateTodo.jsxcomponent is created and imported inApp.jsx.- Test it now: Visit http://localhost:5173 and try creating a new todo. Note: The new todo won’t appear in the list yet (we’ll fix this in the next section), but you should see the form and be able to submit it. Check your browser’s developer console (F12) for any errors.
3. Connect Components Together (Component Communication)
This is the key part! We need to make the Todos component refresh when a new todo is created. This teaches you how React components communicate.
3.1. Understanding the Problem
Right now:
ApprendersTodosandCreateTodoside by side- They don’t know about each other
- When
CreateTodocreates a todo,Todosdoesn’t know to refresh
Solution: Use “state lifting” - manage shared state in the parent (App)
3.2. Add Refresh Mechanism to App
Update ui/src/App.jsx:
import React, { useState } from 'react'; // Add useState
import './globals.css';
import Todos from './Todos';
import CreateTodo from './CreateTodo';
const API_URL = 'http://localhost:8000';
export default function App() {
const [refreshTrigger, setRefreshTrigger] = useState(0); // Add this
function refreshTodos() { // Add this function
setRefreshTrigger((prev) => prev + 1);
}
return (
<>
<header>
<h1>TODO List</h1>
</header>
<main>
<Todos API_URL={API_URL} refreshTrigger={refreshTrigger} /> {/* Add prop */}
<CreateTodo API_URL={API_URL} onTodoCreated={refreshTodos} /> {/* Add prop */}
</main>
</>
);
}
What this does:
- refreshTrigger - A number that changes when we want to refresh
- refreshTodos() - Function that increments the trigger
- Passes
refreshTriggerto Todos (so it knows when to refresh) - Passes
onTodoCreatedto CreateTodo (callback to call after creating)
Key concepts:
- State in parent - App manages the refresh trigger
- Callback props - Passing functions as props
- State changes trigger re-renders - When
refreshTriggerchanges, Todos re-renders
3.3. Update Todos to Watch for Refresh
Update ui/src/Todos.jsx:
import React, { useState, useEffect } from 'react';
export default function Todos({ API_URL, refreshTrigger }) { // Add refreshTrigger prop
const [todoList, setTodoList] = useState([]);
async function fetchTodos() {
const response = await fetch(`${API_URL}/todos`);
const data = await response.json();
setTodoList(data);
}
useEffect(() => {
fetchTodos();
}, [refreshTrigger]); // Add refreshTrigger to dependencies
return (
<div className="todos-container">
<ul>
{todoList.map((todo) => (
<li key={todo.id}>
<div>
{todo.completed ? (
<span>Completed</span>
) : (
<span>In Progress</span>
)}{' '}
{todo.title}
<br />
{todo.description}
</div>
</li>
))}
</ul>
</div>
);
}
What this does:
- Adds
refreshTriggertouseEffectdependencies - When
refreshTriggerchanges,useEffectruns again - This calls
fetchTodos()to get the latest data
Key concepts:
- useEffect dependencies - Array tells React when to re-run the effect
- When
refreshTriggerchanges →useEffectruns →fetchTodos()called → list updates
3.4. Update CreateTodo to Call Callback
Update ui/src/CreateTodo.jsx:
import React, { useState } from 'react';
export default function CreateTodo({ API_URL, onTodoCreated }) { // Add onTodoCreated prop
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
async function handleSubmit(e) {
e.preventDefault();
const response = await fetch(`${API_URL}/todos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description, completed: false }),
});
if (response.ok && onTodoCreated) { // Add check for callback
onTodoCreated(); // Call the callback!
setTitle('');
setDescription('');
}
}
return (
<form className="form-container" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<input
type="text"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<button type="submit">Create TODO</button>
<div className="side-note">
The todo list will automatically update after creating a new todo!
</div>
</form>
);
}
What this does:
- Receives
onTodoCreatedcallback as a prop - Calls it after successfully creating a todo
- This triggers the refresh in the parent (App)
Understanding the complete flow:
- User creates todo →
handleSubmitruns - POST request succeeds →
onTodoCreated()called - This calls
refreshTodos()in App refreshTriggerincrements → Todos’useEffectrunsfetchTodos()called → New list fetched → UI updates!
Before you move on
- Verify your component communication is set up correctly.
- Test it now: Visit http://localhost:5173 and create a new todo. The new todo should automatically appear in the list immediately after you submit the form! If it doesn’t, check your browser’s developer console (F12) for errors and verify your containers are running.
4. Add DELETE Feature
Now we’ll add the ability to delete todos.
4.1. Add Delete Function to Todos
Update ui/src/Todos.jsx:
import React, { useState, useEffect } from 'react';
export default function Todos({ API_URL, refreshTrigger }) {
const [todoList, setTodoList] = useState([]);
async function fetchTodos() {
const response = await fetch(`${API_URL}/todos`);
const data = await response.json();
setTodoList(data);
}
useEffect(() => {
fetchTodos();
}, [refreshTrigger]);
// Add this function
async function deleteTodo(todoId) {
await fetch(`${API_URL}/todos/${todoId}`, {
method: 'DELETE',
});
// Refresh the list to show the todo is gone
fetchTodos();
}
return (
<div className="todos-container">
<ul>
{todoList.map((todo) => (
<li key={todo.id}>
<div>
{todo.completed ? (
<span>Completed</span>
) : (
<span>In Progress</span>
)}{' '}
{todo.title}
<br />
{todo.description}
</div>
{/* Add delete button */}
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
What this does:
- deleteTodo() - Makes DELETE request to remove a todo
- onClick handler - Calls
deleteTodowhen button is clicked - fetchTodos() - Refreshes the list after deletion
Key concepts:
- Event handlers -
onClick={() => deleteTodo(todo.id)} - Arrow functions -
() =>creates a function that callsdeleteTodo - DELETE method - HTTP method for removing resources
4.2. Add Button Styling
Add to ui/src/globals.css:
button {
padding: 5px 10px;
background-color: #ff4444;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
button:hover {
background-color: #cc0000;
}
.side-note {
font-size: 12px;
color: #666;
font-style: italic;
margin-top: 10px;
}
Understanding what just happened:
- User clicks Delete button
deleteTodo(todo.id)is called- DELETE request sent to backend
- Backend removes todo from database
fetchTodos()refreshes the list- Todo no longer appears
Before you move on
- Verify your delete functionality is implemented.
- Test it now: Visit http://localhost:5173, create a test todo if needed, then click the “Delete” button. The todo should immediately disappear from the list. Test this with multiple todos to ensure it works correctly.
5. Final Testing
Now that all features are implemented, test your complete application:
- READ: Visit http://localhost:5173 - todos should load automatically when the page opens
- CREATE: Fill out the form, click “Create TODO”, verify it appears automatically in the list
- DELETE: Click “Delete” on a todo, verify it immediately disappears from the list
Make sure your Docker containers are running:
docker-compose up -d
If you encounter any issues, check:
- Browser developer console (F12) for JavaScript errors
- Backend logs:
docker logs todo_backend - Frontend logs:
docker logs todo_frontend
Before you move on
- Test all CRUD operations: Make sure READ, CREATE, and DELETE all work correctly in your browser.
- Verify that your file structure looks like the one below:
project02-fall2025 ├── .env ├── .git/ ├── backend/ │ └── ... ├── database/ │ └── ... └── ui/ ├── src/ │ ├── App.jsx # updated │ ├── CreateTodo.jsx # new │ ├── globals.css # updated │ ├── main.jsx │ └── Todos.jsx # new ├── Dockerfile ├── index.html ├── package.json └── vite.config.js
6. Summary
How It All Works Together:
The Complete Flow
- Page Loads:
main.jsxrendersAppApprendersTodosandCreateTodoTodos’useEffectruns →fetchTodos()→ GET request → todos displayed
- User Creates Todo:
- User fills form → clicks “Create TODO”
CreateTodo’shandleSubmitruns → POST request → todo createdonTodoCreated()called →App’srefreshTodos()runsrefreshTriggerincrements →Todos’useEffectruns →fetchTodos()→ list updates
- User Deletes Todo:
- User clicks “Delete” →
deleteTodo()runs → DELETE request → todo removed fetchTodos()called → list refreshes → todo gone
- User clicks “Delete” →
Component Communication Pattern
Key Pattern:
- State lifting - Shared state lives in parent (App)
- Props down - Data flows from parent to children
- Callbacks up - Events flow from children to parent via callbacks
Key React Concepts Learned
Hooks
- useState - Manage component state
- useEffect - Handle side effects (API calls, subscriptions)
Component Communication
- Props - Pass data from parent to child
- Callbacks - Pass functions to trigger parent updates
- State lifting - Managing shared state in parent component
Event Handling
- onClick - Handle button clicks
- onChange - Handle input changes
- onSubmit - Handle form submission
Data Fetching
- fetch API - Make HTTP requests
- async/await - Handle asynchronous operations
- JSON - Parse and stringify data
Conditional Rendering
- Ternary operator -
{condition ? true : false} - Logical AND -
{condition && <Component />}
7. Commit and Push
Go ahead and commit / push your changes to git / GitHub.
Before you move on
Verify that all your new code is on GitHub.