A reducer can model loading, success, and error as one status field with a few actions. The reducer stays pure while an event handler or effect performs the request and dispatches the result.
This keeps the UI honest about what the app is doing at every step. The same pattern serves any async work, from fetching a list to saving a form.
One status field, four actions
Give the state a status field that is one of idle, loading, success, or error. Each action moves the status forward and carries the data or message that belongs with it. Keeping the shape small makes each transition obvious and leaves no room for contradictory flags.
const initial = { status: "idle", data: null, error: null };
function reducer(state, action) {
switch (action.type) {
case "started": return { ...state, status: "loading" };
case "succeeded": return { status: "success", data: action.data, error: null };
case "failed": return { status: "error", data: null, error: action.message };
default: return state;
}
}The succeeded case clears error and stores data, while failed clears data and stores the message. Only one outcome can be true at a time because they share one status, so the state can never report success with an error still attached.
Dispatch from the request flow
The reducer never calls fetch. Instead, a handler starts the request, waits for it, and dispatches the outcome.
async function handleLoad() {
dispatch({ type: "started" });
try {
const data = await fetchPosts();
dispatch({ type: "succeeded", data });
} catch (err) {
dispatch({ type: "failed", message: err.message });
}
}Dispatching started shows loading, succeeded stores the posts, and failed stores the error message. Because the reducer only reacts to these actions, it stays pure and easy to test. If the component unmounts before the response arrives, cancel or ignore the stale result so a late dispatch does not update a screen that is gone.
Render each state
Read the status and render a matching view. An empty list is separate from loading, so a successful empty result still shows its own message. Branching on one value keeps the render function short.
if (state.status === "loading") return <p>Loading posts...</p>;
if (state.status === "error") return <p role="alert">{state.error}</p>;
return (
<ul>
{state.data.map((post) => <li key={post.id}>{post.title}</li>)}
</ul>
);While loading, the user sees a status line. On error, the alert paragraph announces the message to screen readers. On success, the list renders with a stable key per post.
The idle state before any request can reuse the same list with an empty prompt, so the screen never looks broken before the first load.
Why a status beats many booleans
Separate flags like isLoading, hasError, and hasData can contradict each other. A single status makes those contradictions impossible and gives you one place to log transitions. One status also turns a flaky request into a readable sequence of started, succeeded, and failed events.
Read why reducers must be pure to see why the request stays outside, or handle loading, error, empty, and success states for the full UI treatment.
Rune AI
Key Insights
- Model loading, success, and error as one status field.
- Dispatch started, succeeded, and failed from the request flow.
- Keep the fetch out of the reducer.
- Render each state with clear UI.
- Start with idle before any request begins.
Frequently Asked Questions
Why use a status field instead of separate booleans?
Should the fetch live inside the reducer?
What does the initial status start as?
Conclusion
Model loading, success, and error as one status field driven by started, succeeded, and failed actions. Keep the request in the handler and let the reducer only decide the next state, which keeps every async transition visible and testable.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.