Let’s create a React component that displays items from your API. Make sure your API is running first!
Step 1: Create the Component
Create web/src/components/ItemList.tsx:
"use client";
import { useEffect, useState } from "react";
interface Item {
id: number;
name: string;
completed: boolean;
}
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
export function ItemList() {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchItems = async () => {
try {
const response = await fetch(`${API_URL}/api/items`);
if (!response.ok) {
throw new Error("Failed to fetch items");
}
const data = await response.json();
setItems(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
};
fetchItems();
}, []);
if (loading) return <div>Loading items...</div>;
if (error) return <div style={{ color: "red" }}>Error: {error}</div>;
return (
<div>
<h2>Items</h2>
{items.length === 0 ? (
<p>No items yet.</p>
) : (
<ul>
{items.map((item) => (
<li key={item.id}>
<input type="checkbox" checked={item.completed} readOnly />
<span>{item.name}</span>
</li>
))}
</ul>
)}
</div>
);
}
Step 2: Use the Component
Update web/src/app/page.tsx:
import { ItemList } from "@/components/ItemList";
export default function Home() {
return (
<main>
<h1>Welcome to my app</h1>
<ItemList />
</main>
);
}
Step 3: Test It
Make sure both services are running:
Terminal 1 (API):
cd api && cargo run
Terminal 2 (Frontend):
cd web && npm run dev
Visit http://localhost:3000 and you should see your items list!
Step 4: Add a Form to Create Items
Create web/src/components/AddItemForm.tsx:
"use client";
import { useState } from "react";
interface Item {
id: number;
name: string;
completed: boolean;
}
interface AddItemFormProps {
onItemAdded: (item: Item) => void;
}
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
export function AddItemForm({ onItemAdded }: AddItemFormProps) {
const [name, setName] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const response = await fetch(`${API_URL}/api/items`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
if (!response.ok) {
throw new Error("Failed to create item");
}
const newItem = await response.json();
setName("");
onItemAdded(newItem);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} style={{ marginBottom: "20px" }}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="What needs to be done?"
disabled={loading}
/>
<button type="submit" disabled={loading || !name}>
{loading ? "Adding..." : "Add"}
</button>
{error && <div style={{ color: "red" }}>{error}</div>}
</form>
);
}
Step 5: Combine Form + List
Update web/src/components/ItemList.tsx:
"use client";
import { useEffect, useState } from "react";
import { AddItemForm } from "./AddItemForm";
interface Item {
id: number;
name: string;
completed: boolean;
}
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
export function ItemList() {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchItems = async () => {
try {
const response = await fetch(`${API_URL}/api/items`);
if (!response.ok) throw new Error("Failed to fetch");
const data = await response.json();
setItems(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
};
fetchItems();
}, []);
const handleItemAdded = (newItem: Item) => {
setItems([...items, newItem]);
};
if (loading) return <div>Loading...</div>;
if (error) return <div style={{ color: "red" }}>Error: {error}</div>;
return (
<div>
<h2>Items</h2>
<AddItemForm onItemAdded={handleItemAdded} />
{items.length === 0 ? (
<p>No items yet.</p>
) : (
<ul>
{items.map((item) => (
<li key={item.id}>
<input type="checkbox" checked={item.completed} readOnly />
<span>{item.name}</span>
</li>
))}
</ul>
)}
</div>
);
}
Now you can add items from the form! 🎉
Step 6: With Tailwind CSS (Optional)
If you scaffolded with --tailwind:
export function ItemList() {
// ... state and logic ...
return (
<div className="max-w-md mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">Items</h2>
<AddItemForm onItemAdded={handleItemAdded} />
{items.length === 0 ? (
<p className="text-gray-500 mt-4">No items yet.</p>
) : (
<ul className="space-y-2 mt-4">
{items.map((item) => (
<li
key={item.id}
className="flex items-center gap-3 p-3 border rounded hover:bg-gray-50"
>
<input type="checkbox" checked={item.completed} className="w-5 h-5" />
<span className={item.completed ? "line-through text-gray-500" : ""}>
{item.name}
</span>
</li>
))}
</ul>
)}
</div>
);
}
Update the form too:
return (
<form onSubmit={handleSubmit} className="flex gap-2 mb-4">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="What needs to be done?"
disabled={loading}
className="flex-1 px-4 py-2 border rounded"
/>
<button
type="submit"
disabled={loading || !name}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{loading ? "Adding..." : "Add"}
</button>
{error && <div className="text-red-500">{error}</div>}
</form>
);
Understanding the Code
"use client"
Marks this as a Client Component because it uses:
useStateanduseEffect(React hooks)- Event handlers (form submission)
- Browser APIs (fetch)
useEffect Hook
Runs once on mount ([] dependency array) to fetch items from your API.
State Management
items— The list of itemsloading— Show loading state while fetchingerror— Show error message if fetch fails
Error Handling
Wraps fetch in try-catch to handle network errors gracefully.
Common Issues
“Items don’t appear”
- Check your API is running on port 3001
- Check
NEXT_PUBLIC_API_URLin.env.local - Check browser console (F12) for errors
“CORS error”
- Your API needs to allow requests from localhost:3000
- See Troubleshooting
“Form doesn’t work”
- Make sure
onItemAddedhandler is called - Check that your API POST endpoint returns the created item
Next Steps
- Full-Stack Integration — How API and frontend work together
- Frontend Patterns — Advanced React patterns
- API Patterns — Build better endpoints
- Deploy to Railway — Ship your app