Load the user profile card
React · React · beginner · greenfield
Adds the profile card with the full async-state triangle: loading, error-with-retry, and the loaded card. HTTP errors are surfaced by checking `res.ok` (fetch doesn't reject on a 500), and a `cancelled` flag in the effect ignores responses that arrive after unmount. The parent keys the card by user id, so each user gets a fresh mount.
Requirements
- Build `ProfileCard({ userId })`: fetch `/api/users/{userId}` and render the profile. `userId` is a server-issued id string; URL-encode it when building the request path.
- The parent renders the card as `<ProfileCard key={userId} userId={userId} />`, so a changed user id **remounts** the component — `userId` never changes within one component instance.
- A successful (2xx) response is JSON of shape `{ name: string, email: string }` — both fields always present for an existing user.
- Handle all three async states: a loading message while the request is in flight, an error state for network failures **and** non-2xx responses, and the profile card on success.
- The error state offers a Retry button that re-issues the request, and is announced to assistive tech (`role="alert"`).
- A response that lands after the component has unmounted must not update state.
- `fetch` rejects only on network failure; HTTP error statuses resolve normally and must be checked via `res.ok`.
Files touched
- src/components/ProfileCard.jsx
--- src/components/ProfileCard.jsx
+import { useEffect, useState } from 'react';
+
+export function ProfileCard({ userId }) {
+ const [profile, setProfile] = useState(null);
+ const [status, setStatus] = useState('loading');
+ const [attempt, setAttempt] = useState(0);
+
+ useEffect(() => {
+ let cancelled = false;
+ setStatus('loading');
+ fetch(`/api/users/${encodeURIComponent(userId)}`)
+ .then((res) => {
+ if (!res.ok) {
+ throw new Error(`HTTP ${res.status}`);
+ }
+ return res.json();
+ })
+ .then((data) => {
+ if (!cancelled) {
+ setProfile(data);
+ setStatus('ready');
+ }
+ })
+ .catch(() => {
+ if (!cancelled) {
+ setStatus('error');
+ }
+ });
+ return () => {