useEffect - Side Effects, Data Fetching & Async State
Browser DevTools - The Network Tab
Content
Difficulty: Easy Concepts: useEffect, data fetching, async state (loading, error, success)
Build a component that fetches and displays a list of items from a public REST API of your choice.
Requirements:
Pick any public REST API that returns a list of items — JSONPlaceholder, PokeAPI, Open Library, REST Countries or anything else you find interesting. No API key should be required.
Create a React component that fetches data inside a useEffect with the correct dependency array
Model your async state as a TypeScript discriminated union with three states:
type FetchState<T> = | { status: 'loading' } | { status: 'error'; message: string } | { status: 'success'; data: T };
Render a loading message while the request is in flight
Render a meaningful error message if the request fails
Render the data as a list once it arrives (at minimum show the name or title of each item)
Check response.ok before parsing JSON and throw an error if it's false
Hints:
async directly on the useEffect callback: define an async function inside it and call it{ status: 'loading' } at the start of the effect so the loading state resets if a dependency changesDifficulty: Medium Concepts: useEffect dependencies, cleanup, AbortController, controlled inputs
Extend what you built in Exercise 1 into a searchable interface where the fetch re-runs based on user input.
Requirements:
Add a text input that controls a query state variable
The useEffect should re-fetch whenever query changes: query must be in the dependency array
If query is empty, skip the fetch and reset to an idle state instead of showing a loading spinner
Add AbortController cleanup so that changing the query while a fetch is in flight cancels the previous request:
useEffect(() => { if (!query.trim()) { setState({ status: 'idle' }); return; } const controller = new AbortController(); // ... fetch with { signal: controller.signal } return () => controller.abort();}, [query]);
Handle the AbortError separately: a cancelled request is not a real error and should not update the error state
All three states (loading, error, success) plus the idle state must be handled in the render
Hints:
error.name === 'AbortError' is how you detect a cancelled request in a .catch() handlerasync/await, wrap in try/catch and check error instanceof Error && error.name !== 'AbortError'setState({ status: 'loading' }) before starting the fetch but after the empty-query guard, so the loading state is visible while the request is in flightDifficulty: Easy Concepts: Browser DevTools, Network tab, watching fetch calls, HTTP status codes
Use the browser's Network tab to observe and understand what your Exercise 2 component is actually doing over the network.
Requirements:
Open your Exercise 2 component in the browser with DevTools open on the Network tab, filtered to Fetch/XHR. Complete each of the following tasks and write down your answers in a comment block at the top of your component file:
(cancelled) status? This is your AbortController cleanup working. If you don't see any cancellations, try typing faster./userss instead of /users). What status code appears in the Network tab? What does the Preview tab show: is it JSON or something else?Set throttling back to No throttling when you're done.
Hints:
Difficulty: Easy Concepts: Tailwind utility classes, how Tailwind maps to CSS properties
Convert the CSS below to Tailwind utility classes applied directly to the JSX. The goal is to produce a visually identical result without writing any CSS.
Starting point — the CSS:
.card {
background-color: #ffffff;
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
max-width: 28rem;
width: 100%;
}
.card-title {
font-size: 1.25rem;
font-weight: 700;
color: #111827;
margin-bottom: 0.5rem;
}
.card-body {
font-size: 0.875rem;
color: #6b7280;
line-height: 1.625;
margin-bottom: 1.5rem;
}
.card-button {
display: inline-block;
background-color: #2563eb;
color: #ffffff;
font-size: 0.875rem;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: none;
cursor: pointer;
}
.card-button:hover {
background-color: #1d4ed8;
}
@media (min-width: 640px) {
.card {
padding: 2rem;
}
}
Starting point — the JSX (with CSS classes):
export default function Card() {
return (
<div className="card">
<h2 className="card-title">Getting started</h2>
<p className="card-body">
This is a simple card component. It has a title, some body text, and a call-to-action button below.
</p>
<button className="card-button">Learn more</button>
</div>
);
}
Requirements:
className that reference a stylesheet (make sure you have empty className attributes so you can fill them with Tailwind utility classes)sm: breakpoint) must be includedw-[28rem]) where a built-in Tailwind class exists: look up the closest match in the Tailwind docsHints:
p-6 = 1.5rem, p-8 = 2rem, mb-2 = 0.5rem, mb-6 = 1.5remtext-gray-500 is #6b7280, text-gray-900 is #111827, bg-blue-600 is #2563eb, hover:bg-blue-700 is #1d4ed8max-w-md = 28rem (Tailwind has named max-width sizes worth looking up)The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

Built with ❤️ by the HackYourFuture community · Thank you, contributors
Found a mistake or have a suggestion? Let us know in the feedback form.