React #10 — Typescript — useReducer
In this article, we will build the data layer for a quiz app using React and TypeScript. The flow is simple: JSON Server will provide mock…
React #10 — Typescript — useReducer
In this article, we will build the data layer for a quiz app using React and TypeScript. The flow is simple: JSON Server will provide mock quiz questions, Zod will validate the API response, a service function will fetch the data, and a custom hook will use useReducer to manage the request state.
[embed]List: React — TS | Curated by quizzesforyou.com | Medium React — TS · 9 stories on Mediummedium.com
What we are building
The app needs to load a list of quiz questions from an API and track whether the request is loading, ready, failed, active, or finished. This is a perfect use case for useReducer, because the state has a few related values that should change together.
- JSON Server exposes the quiz data from
data/questions.json. - Zod schemas define the expected shape of each question.
- A service function fetches and validates the data.
- A custom hook stores the questions, status, and error message in one reducer state.

1. Create a JSON Server
First, create a data folder and add a questions.json file inside it. This file will act as our small local database while we build the front end.
Install JSON Server:
npm i json-server
Then add a script in package.json:
"server": "json-server --watch data/questions.json --port 8000"
Now run the server:
npm run server
JSON Server will start on port 8000. If your JSON file has a questions key, the endpoint will be available at:
http://localhost:8000/questions
This gives us a realistic API while keeping the setup lightweight.
2. Create Schemas and Types
TypeScript helps us describe what we expect in our code, but it does not validate data at runtime. An API can still return unexpected data. That is where Zod is useful: it checks the response while the app is running.
Create a schema for one question:
import z from "zod";
export const questionSchema = z.object({
question: z.string(),
options: z.array(z.string()),
correctOption: z.number(),
points: z.number(),
id: z.string()
});
Then create a schema for the full list of questions:
import z from "zod";
import { questionSchema } from "./question.schema";
export const questionsSchema = z.array(questionSchema);
Finally, infer the TypeScript type from the schema:
import z from "zod";
import { questionSchema } from "../schemas";
export type Question = z.infer<typeof questionSchema>;
This keeps the schema and TypeScript type connected. If the schema changes later, the type updates with it.
3. Create the Questions Service
The service is responsible for calling the API, checking the HTTP response, validating the JSON, and returning clean data to the rest of the app.
import { API_Config } from "../appconfig";
import { questionsSchema } from "../shared/schemas";
import { Question } from "../shared/types/question.types";
/**
* @param signal
* @returns Promise<Question[]>
*/
export const getQuestions = async (signal?: AbortSignal): Promise<Question[]> => {
const response = await fetch(API_Config.baseUrl + "/questions", { signal });
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
const data = await response.json();
const result = questionsSchema.safeParse(data);
if (!result.success) {
console.error(result.error);
throw new Error("Invalid API Response");
}
return result.data;
};
The important part here is safeParse. Zod has two common validation methods:
parsethrows an error when the data is invalid.safeParsereturns a result object, so we can handle validation failure without crashing immediately.
The result looks like this:
{
success: boolean;
data?: T;
error?: ZodError;
}
This makes the service predictable: either it returns valid Question[] data, or it throws an error that the UI can handle.
4. Create a Custom Hook with useReducer
Now we can create a custom hook called useQuestions. This hook owns the request state and exposes it to the UI.
Instead of keeping questions, status, and error in separate state variables, we keep them together in one reducer state. That makes every transition easier to understand.
import { useEffect, useReducer } from "react";
import { getQuestions } from "../services/questionsService";
import { Question } from "../shared/types/question.types";
type State = {
questions: Question[];
status: "loading" | "error" | "ready" | "active" | "finished";
error: string | null;
};
type Action =
| { type: "dataReceived"; payload: Question[] }
| { type: "dataFailed"; payload: string }
| { type: "loading" };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "dataReceived":
return {
...state,
questions: action.payload,
status: "ready",
error: null
};
case "dataFailed":
return {
...state,
questions: [],
status: "error",
error: action.payload
};
case "loading":
return {
...state,
status: "loading"
};
default:
return state;
}
};
export const useQuestions = (query: string) => {
const initialState: State = {
questions: [],
status: "loading",
error: null
};
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
if (!query) return;
const controller = new AbortController();
const fetchQuestions = async () => {
dispatch({ type: "loading" });
try {
const data = await getQuestions(controller.signal);
dispatch({ type: "dataReceived", payload: data });
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
return;
}
dispatch({
type: "dataFailed",
payload: err instanceof Error ? err.message : "Something went wrong"
});
}
};
fetchQuestions();
return () => {
controller.abort();
};
}, [query]);
return state;
};
Why useReducer works well here
The reducer gives the data flow a clear structure:
- When the request starts, we dispatch
loading. - When the data comes back successfully, we dispatch
dataReceived. - If something fails, we dispatch
dataFailed.
[embed]List: React - TS | Curated by quizzesforyou.com | Medium React - TS · 9 stories on Mediummedium.com
메타데이터
- post_id
- 6c31ce7ef630
- slug
- react-10-typescript-usereducer-6c31ce7ef630
- url
- https://medium.com/@quizzesforyou/react-10-typescript-usereducer-6c31ce7ef630
- canonical_url
- https://medium.com/@quizzesforyou/react-10-typescript-usereducer-6c31ce7ef630
- author_url
- https://medium.com/@quizzesforyou
- status
- ok
- fetched_at
- 2026-06-24 11:06:28