Practice Questions
1. When a component fetches data on mount using useEffect, the data starts as null and arrives asynchronously. Describe the render lifecycle: how many times does the component render, and what does it display at each stage? Why must you handle the null state in your JSX?
Solution
The component renders at least twice:
- First render (mount): The component renders with the initial state (
null). TheuseEffectcallback is scheduled but has not run yet, so no data is available. The JSX must handle this, typically by showing “Loading…” or nothing. - After the fetch resolves: The
.then()callback callssetStatewith the fetched data. This triggers a second render with the actual data, and the component can now display it.
You must handle the null state because React renders the component before the effect runs. If your JSX tries to access a property on null (e.g., data.name), it will throw a runtime error. You can handle it with conditional rendering (data ? <Content /> : <Loading />), optional chaining (data?.name), or an early return (if (!data) return <p>Loading...</p>).
2. Write a React component called UserGreeting that fetches user data from https://api.example.com/users/1 when the component mounts and displays the user’s name. Use useState and useEffect.
Solution
import { useEffect, useState } from "react";
interface User {
name: string;
}
const UserGreeting = () => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch("https://api.example.com/users/1")
.then((res) => res.json())
.then((data) => setUser(data));
}, []);
return <h1>{user ? `Hello, ${user.name}!` : "Loading..."}</h1>;
};
export default UserGreeting;
Key points: useState initializes to null since we have no data yet. useEffect with [] ensures the fetch happens once on mount. We handle the null state with a conditional expression.
3. What is the purpose of defining a TypeScript interface to model an API response? Why not just use any or skip the type entirely?
Solution
A TypeScript interface gives you a few things:
- Autocompletion: your editor can suggest property names and catch typos.
- Type checking at compile time: if you try to access a property that does not exist on the interface, TypeScript flags the error before you run the code.
- Documentation: the interface describes the shape of the data, so you can see which fields are available.
- Mapping raw responses: when an API returns many fields but your app only needs a few, you can define a smaller internal interface and map the response to it. This decouples your UI from the API’s exact shape.
Using any removes all of these benefits. You lose safety and discoverability.
4. Given the following API response type and internal type, write a service function that fetches from the API and maps the response to the internal type.
interface ApiBook {
volumeInfo: {
title: string;
authors: string[];
publishedDate: string;
imageLinks: { thumbnail: string };
};
}
interface Book {
title: string;
author: string;
year: string;
cover: string;
}
Solution
const API_URL = "https://api.example.com/books";
export async function fetchBook(id: string): Promise<Book> {
const response = await fetch(`${API_URL}/${id}`);
if (!response.ok) {
throw new Error(`API request failed with status: ${response.status}`);
}
const data: ApiBook = await response.json();
return {
title: data.volumeInfo.title,
author: data.volumeInfo.authors.join(", "),
year: data.volumeInfo.publishedDate,
cover: data.volumeInfo.imageLinks.thumbnail,
};
}
The function fetches raw API data typed as ApiBook, checks for errors, then maps only the needed fields into the simpler Book shape. This decouples your component from the exact structure of the external API.
5. Write a component called ItemList that receives an array of { id: number; name: string } objects as a prop and renders them as a <ul>. Make sure to handle the key prop correctly.
Solution
interface Item {
id: number;
name: string;
}
interface ItemListProps {
items: Item[];
}
const ItemList = ({ items }: ItemListProps) => {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
};
export default ItemList;
Each <li> needs a unique key so React can efficiently track which items change, are added, or are removed. The item’s id is a good key because it is stable and unique. Do not use the array index as a key when the list can change, because that can cause incorrect re-renders.
6. Describe the complete data flow when a React app fetches data from an API and displays it. Trace the path from the service function through state management to the rendered output. What role does each piece (service function, useEffect, useState, props) play?
Solution
The data flows through these steps:
- Service function – a standalone async function (in a
services/module) that callsfetch, checks the response, and maps the raw JSON to a well-typed internal model. It knows nothing about React. useEffect– the component calls the service function insideuseEffect(typically with[]for on-mount fetching or[dep]to re-fetch when a dependency changes). This keeps the side effect out of the render path.useState– the effect’s.then()callback stores the fetched data in state viasetState. This triggers a re-render with the new data.- Props – the component that owns the data passes it down to child components via props. Children render the data or pass it further down. If a child needs to trigger a new fetch (e.g., selecting a different item), the parent passes a callback prop that updates state, which changes the
useEffectdependency and triggers a re-fetch.
Each piece has a single responsibility: the service handles I/O and data shaping, useEffect manages timing, useState holds the data and triggers renders, and props connect components in a one-way flow.
7. Consider two sibling components: SearchBar and ResultsList. When the user types a query in SearchBar, ResultsList should display matching results. Write the parent App component that wires these two together. You do not need to implement SearchBar or ResultsList – just show the App component with the state and the props it passes.
Solution
import { useState } from "react";
import SearchBar from "./SearchBar";
import ResultsList from "./ResultsList";
const App = () => {
const [query, setQuery] = useState("");
return (
<div>
<SearchBar onQueryChange={setQuery} />
<ResultsList query={query} />
</div>
);
};
export default App;
App owns the query state. It passes setQuery (or a wrapper callback) to SearchBar so the child can notify the parent when the user types. It passes the current query value to ResultsList so it can filter or fetch results. This is the same lifting-state-up pattern: state down, events up.
8. Write a component called PriceDisplay that accepts a productId prop. It should fetch the price from https://api.example.com/products/{productId} and display it. The fetch should re-run whenever productId changes.
Solution
import { useEffect, useState } from "react";
interface PriceDisplayProps {
productId: string;
}
const PriceDisplay = ({ productId }: PriceDisplayProps) => {
const [price, setPrice] = useState<number | null>(null);
useEffect(() => {
fetch(`https://api.example.com/products/${productId}`)
.then((res) => res.json())
.then((data) => setPrice(data.price));
}, [productId]);
return <p>{price !== null ? `$${price.toLocaleString()}` : "Loading..."}</p>;
};
export default PriceDisplay;
The key detail is [productId] in the dependency array. This tells React to re-run the effect whenever productId changes, triggering a new fetch. Without it (using []), the component would only fetch once on mount and never update when a different product is selected.
9. What is optional chaining (?.) and why is it useful when working with data fetched from an API? Give an example.
Solution
Optional chaining (?.) short-circuits to undefined if the value on the left is null or undefined, instead of throwing a runtime error.
It is especially useful with fetched data because the data starts as null before the API responds. Without optional chaining, accessing a property on null would crash the component.
const [user, setUser] = useState<{
name: string;
address: { city: string };
} | null>(null);
// Without optional chaining -- crashes if user is null:
// const city = user.address.city;
// With optional chaining -- safely returns undefined:
const city = user?.address?.city;
This allows the component to render safely (e.g., showing nothing or a fallback) while the data is still loading.
10. What is the benefit of creating a separate “service” module (e.g., services/api.ts) for your fetch functions rather than writing fetch calls directly inside your components?
Solution
Putting the fetch logic in a service module helps in a few ways:
- Reusability: multiple components can call the same service function without duplicating the fetch logic.
- Separation of concerns: components focus on rendering UI, while service modules handle data fetching and transformation. This makes each file easier to read and maintain.
- Easier testing: you can test the service function in isolation, mocking
fetchwithout rendering a component. - Centralized error handling: error checking (like
if (!response.ok)) and response mapping live in one place rather than being scattered across components. - Type safety: the service function can type the raw API response internally and return a clean, well-typed object that components consume, decoupling the UI from the API shape.
11. A teammate writes the following component. It is supposed to fetch a list of posts when the component mounts, but it has a bug. Identify the problem and fix it.
const PostList = () => {
const [posts, setPosts] = useState([]);
const response = fetch("https://api.example.com/posts");
const data = response.json();
setPosts(data);
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
Solution
There are two problems:
- Side effect outside
useEffect: the fetch runs directly in the component body, so it executes on every render. CallingsetPoststriggers a re-render, which runs the fetch again, causing an infinite loop. - Not handling the Promise:
fetchreturns a Promise, but the code treats the result as if it were synchronous. You need.then()orawaitto get the actual data.
Fixed version:
import { useEffect, useState } from "react";
interface Post {
id: number;
title: string;
}
const PostList = () => {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
fetch("https://api.example.com/posts")
.then((res) => res.json())
.then((data) => setPosts(data));
}, []);
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
export default PostList;
Wrapping the fetch in useEffect with [] ensures it runs once on mount, and .then() properly handles the asynchronous response.