For detail pages (e.g., /users/3), fetch one record by ID:
Backend (Python)
API Client (JS)
Copy
@app.get("/api/users/{user_id}")def get_user(user_id: int): user = find_user(user_id) if not user: raise HTTPException(status_code=404, detail="User not found") return user
Copy
export async function getUser(id) { const response = await fetch(`${API_URL}/api/users/${id}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json();}
Copy
function UserDetail({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function loadUser() { try { const data = await getUser(userId); setUser(data); } catch (err) { setError(err.message); } finally { setLoading(false); } } loadUser(); }, [userId]); // Re-fetch when userId changes if (loading) return <p>Loading...</p>; if (error) return <p className="error">{error}</p>; if (!user) return <p>User not found</p>; return ( <div> <h2>{user.name}</h2> <p>{user.email}</p> <p>User ID: {user.id}</p> </div> );}
Notice [userId] in the dependency array. If the user navigates from one profile to another, userId changes and the effect re-runs — fetching the new user automatically.
The smart component (UserList) handles state and fetching. The presentational component (UserCard) just displays data. This is the composition pattern from the React Essentials section.
The companion repo’s User shape is intentionally simple: id, name, and email. If you add fields later (like role), this same read pattern still works — you just render the new properties.
Fetch the full list once, then filter in the browser. This is fast for small-to-medium lists (hundreds of items). For large datasets, filter on the backend with query parameters.
For small datasets (under ~500 items), client-side filtering is simpler and faster — no extra API calls on every keystroke. For large datasets, send the search term as a query parameter: GET /api/users?search=sarah.
Sometimes you need to reload data — after creating, updating, or deleting:
Copy
function UserDashboard() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); async function loadUsers() { setLoading(true); try { const data = await getUsers(); setUsers(data); } finally { setLoading(false); } } useEffect(() => { loadUsers(); }, []); async function handleUserCreated(newUser) { // Option 1: Add to local state (instant, no extra request) setUsers(prev => [...prev, newUser]); // Option 2: Refetch the full list (always in sync with backend) // await loadUsers(); } return ( <div> <CreateUserForm onUserCreated={handleUserCreated} /> {loading ? <p>Loading...</p> : ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> )} </div> );}
Option 1 (update local state) is faster. Option 2 (refetch) is simpler and always correct. Start with local state updates, and switch to refetching if you run into sync issues.