React Screens That Load Data
Sprout's dashboard is a React component that shows the user's plants. The plants live on the server, so the component starts with nothing and fills in a moment later, or fails to. Handling that gap well is what separates a solid screen from one that flashes blank, spins forever or crashes.
The rule
Every data screen has three states: loading, failed, ready.
import { useEffect, useState } from 'react';
export function PlantList() {
const [plants, setPlants] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/plants')
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setPlants)
.catch(setError);
}, []);
if (error) return <p>Could not load your plants.</p>;
if (plants === null) return <p>Loading…</p>;
if (plants.length === 0) return <p>No plants yet. Add your first one.</p>;
return <ul>{plants.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}useState(null):nullmeans "not loaded yet", which is different from[], "loaded, and there are none". Two different situations deserve two different screens.useEffect(..., []): runs the fetch after the component first appears. The empty array means it does not run again on later redraws.- The early returns handle each state before the happy path. An empty list is a special case of "ready" that deserves a friendly nudge.
key={p.id}: the database id is a stable key, so React can tell list items apart when they change.
A good failed state also offers a way out, such as a "Try again" button that runs the same fetch once more. A message with no next step leaves the user stuck.
One idea matters more than the syntax: the server is the source of truth, and React state is a copy. After any change, such as adding, watering or deleting a plant, either put the server's response into state or fetch the list again. Never let the copy drift from what the server said.
Data-fetching libraries such as TanStack Query and SWR wrap this pattern with caching and retries, but underneath they track the same three states.
The shortcut
Before writing the happy-path JSX, write the early returns first. If you cannot say what the screen shows while loading and after a failure, the screen is not designed yet.
First render
plantsisnullanderrorisnull, so the user sees "Loading…".
Drills
Answer fast — trust the rule.
Why does plants start as null instead of []?
What does the empty array in useEffect(fn, []) mean here?
The server replies 404 with the body {"error": "not found"}, and the code has no res.ok check. What happens?
A POST that adds a plant succeeds. What keeps the dashboard honest?
Remember
- Every data screen has three states: loading, failed, ready (and ready may be empty).
- Shortcut: write the early returns before the happy path.
- Trap: treating React state as the truth. It is a copy of the server's data; refresh it from the server's answers.
