A few days ago, I decided to stop doing toy examples and actually build something real with useReducer.
So I built a React Quiz App.
Not a huge app. Not a fancy app.
But a real app with real state, real flows, and real problems.
And this time, I didn’t even hardcode the data.
I used a fake API with json-server to simulate a real backend:
"json-server": "^0.17.3"
So the app fetches questions exactly like a real production app would.
And honestly? I enjoyed this more than I expected.
This was the first time useReducer didn’t feel like “just another hook”…
It finally felt like an architectural tool.
The Problem: My App Was No Longer “Simple State”
This quiz app has:
- Loading state
- Error state
- Ready state
- Active (playing) state
- Questions coming from an API (json-server)
- Current question index
- Selected answer
That’s not one state.
That’s a state machine.
And this is exactly where useReducer starts to make sense.
Instead of doing this:
const [status, setStatus] = useState("loading");
const [questions, setQuestions] = useState([]);
const [index, setIndex] = useState(0);
const [answer, setAnswer] = useState(null);
I moved everything into one predictable state object:
const initialState = {
questions: [],
status: "loading",
index: 0,
answers: null,
};
And one central brain to control everything: the reducer.
The Breakthrough: One Place That Controls the Whole App
This part was the real “aha” moment for me.
Instead of updating state from everywhere, I now do this:
function reducer(state, action) {
switch (action.type) {
case "setQuestions":
return { ...state, questions: action.payload, status: "ready" };
case "setError":
return { ...state, status: "error" };
case "setStart":
return { ...state, status: "active" };
case "newAnswer":
return { ...state, answers: action.payload };
default:
throw new Error("Unknown action");
}
}
And in my app:
const [state, dispatch] = useReducer(reducer, initialState);
Now:
- Components don’t change state directly
- They just dispatch events
- The reducer decides what is allowed to happen
This feels very close to real software architecture, not just React tricks.
The Best Part: My UI Became a Pure Reflection of State
Look at this logic:
{status === "loading" && <Loader />}
{status === "error" && <Error />}
{status === "ready" && <StartScreen />}
{status === "active" && <Question />}
I’m not thinking in terms of “what function should I call”.
I’m thinking in terms of:
“What state is the app in right now?”
That mental shift is huge.
Now the app feels:
- More predictable
- Easier to debug
- Easier to extend
- More professional
The Real Lesson
Before this project, useReducer felt like:
“Powerful, but overkill.”
After this project, it feels like:
“This is how you control complex state like an engineer.”
This quiz app taught me:
- How to design state, not just store it
- How to centralize logic
- How to think in events and transitions, not random setters
- How to work with a real API flow using
json-server
And most importantly:
I’m no longer scared of
useReducer.
I’m actually excited to use it again.
Final Thoughts
This wasn’t a big project.
But it was a big step in how I think about React.
I’m slowly moving from:
“Making things work”
to:
“Designing things properly”
And that’s the real progress.
Top comments (1)
This is a great example of when
useReduceractually clicks.The moment you described the app as a state machine instead of “a few pieces of state” is exactly the mental shift most people miss.
I really like how you framed the reducer as a central brain that decides what’s allowed to happen. That’s the point where React stops feeling like a collection of hooks and starts feeling like real application architecture.
Also +1 for using
json-server. Adding async flows and error states is usually what exposes the limits ofuseState, and you showed that transition very clearly.This kind of “real app, real problems” write-up is way more helpful than abstract examples. Looking forward to seeing where you apply this pattern next.