Skip to main content
Career Paths
Concepts
Frontend Project Intermediate
The Simplified Tech

Role-based learning paths to help you master cloud engineering with clarity and confidence.

Product

  • Career Paths
  • Interview Prep
  • Scenarios
  • AI Features
  • Cloud Comparison
  • Pricing

Community

  • Join Discord

Account

  • Dashboard
  • Credits
  • Updates
  • Sign in
  • Sign up
  • Contact Support

Stay updated

Get the latest learning tips and updates. No spam, ever.

Terms of ServicePrivacy Policy

© 2026 TheSimplifiedTech. All rights reserved.

BackBack
Interactive Explainer

Build Challenge: GitHub User Search

Your Day 1 task at a startup: build a GitHub user search in React. The team code-reviews it on Day 5. Ship something a senior engineer respects.

Relevant for:JuniorMid-levelSenior
Why this matters at your level
Junior

Build this project end to end. The debounce + AbortController pattern will appear in your first code review at every company. Having already debugged the race condition is the difference between "I have heard of this" and "I fixed this before."

Mid-level

Extend the project: add React Query to replace the manual fetch/status state machine, add pagination, and add an integration test that mocks the GitHub API. This is the version you present in system design discussions.

Senior

Add a cache layer (SWR or React Query staleTime), rate limit handling with exponential backoff, and a server-side proxy route that adds a GitHub token. Discuss the tradeoffs of client-side vs server-side fetching for this use case.

Build Challenge: GitHub User Search

Your Day 1 task at a startup: build a GitHub user search in React. The team code-reviews it on Day 5. Ship something a senior engineer respects.

~5 min read
Be the first to complete!
LIVEReal First-Week Task
Breaking News
Day 1

Task assigned: GitHub user search in React, public API, deployed by Friday.

Day 3

Demo works. Types a username, sees the avatar and profile. Engineer is confident.

Day 4

Code review: 23 comments. Race condition on fast typing. No debounce. Tests check component state, not user behaviour. Error state shows raw JSON.

CRITICAL
Day 5

After fixes: debounce added, AbortController cancels stale requests, RTL tests pass, error message is human-readable. Merged.

—code review comments on a "working" demo — all fixable with patterns from this challenge
—to build something a senior engineer merges without hesitation

The question this raises

When the demo works but the code review has 23 comments, what does "production-ready" actually mean for a frontend feature?

Test your assumption first

A user types "torvalds" character by character in a search input. The component fires a fetch on every onChange event and calls setState with the response. The user sees "torval" displayed even though they finished typing "torvalds". What is the root cause?

Lesson outline

Before frameworks: why this was painful

Before React and the fetch API, building a live search against an external API required XMLHttpRequest, manual DOM updates, and careful management of "which response belongs to which keypress."

How this concept changes your thinking

Situation
Before
After

Fetching data on user input

“XMLHttpRequest with onreadystatechange callbacks. Nest error handling inside success handler. Manual abort via xhr.abort() if you remembered.”

“fetch() returns a Promise. AbortController cancels stale requests. async/await makes the sequence readable. React re-renders on state change automatically.”

Showing loading / error / success states

“Manually toggle CSS classes on DOM nodes. Three separate variables, easy to get out of sync. Spinner stays visible if you forget to hide it.”

“useState for status: "idle" | "loading" | "success" | "error". Single source of truth. Component re-renders to the correct UI automatically.”

Testing the component

“Hard to test DOM manipulation code. Unit tests often mocked the entire DOM. Brittle — broke on any refactor.”

“React Testing Library: render the component, type into the input, assert on what the user sees. Tests survive refactors because they test behaviour.”

The naive approach: what the 23 comments were about

The demo worked. The code had three race conditions and no error handling. Here is the exact pattern that gets flagged in every React code review:

GitHubSearch.tsx (before)
1// The demo-works-but-fails-code-review version
2function GitHubSearch() {
3 const [query, setQuery] = useState('');
4 const [user, setUser] = useState(null);
5
6 // Problem 1: fires on every keystroke — 26 requests for "torvalds"
7 // Problem 2: responses arrive out of order — race condition
8 // Problem 3: no loading state, no error state
9 async function handleChange(e) {
No debounce: 26 requests for "torvalds", GitHub rate-limits after 60/hr unauthenticated
10 setQuery(e.target.value);
Template literal in fetch: works but no AbortController means stale responses overwrite current state
11 const res = await fetch(`https://api.github.com/users/${e.target.value}`);
12 const data = await res.json();
Race condition: if "torvalds" response arrives before "t", "t" state wins
13 setUser(data); // which request's data is this?
14 }
15
16 return (
17 <div>
No loading state: user sees nothing while fetch is in flight
18 <input onChange={handleChange} value={query} />
19 {/* Problem 4: shows raw API error object when user not found */}
20 {user && <p>{user.name || JSON.stringify(user)}</p>}
21 </div>
22 );
23}

missing-abort-controller

Bug
useEffect(() => {
  fetch(`/api/users/${query}`)
    .then(r => r.json())
    .then(setUser);
}, [query]);
Fix
useEffect(() => {
  if (!query.trim()) return;
  const controller = new AbortController();
  setStatus('loading');
  fetch(`/api/users/${query}`, { signal: controller.signal })
    .then(r => r.ok ? r.json() : Promise.reject(r.status))
    .then(data => { setUser(data); setStatus('success'); })
    .catch(err => { if (err.name !== 'AbortError') setStatus('error'); });
  return () => controller.abort();
}, [query]);

The cleanup function returned from useEffect runs before the next effect fires, aborting the previous fetch. This eliminates the race condition. AbortError is filtered out because it is expected and not a real failure.

What the browser shows you (and what GitHub shows you)

Two things break silently with the naive approach. The Network tab exposes both.

Network tab on fast typing (no debounce, no AbortController)

Open DevTools Network tab, filter to "Fetch/XHR", then type "torvalds" quickly. You will see: 8 requests fire in parallel. 7 are cancelled (or not — depends on timing). The responses arrive out of order: request 5 ("torval") resolves after request 8 ("torvalds"). Result: the UI shows the "torval" user, not "torvalds". Silent data corruption. GitHub rate limit header: X-RateLimit-Remaining: 0 After 60 unauthenticated requests per hour, all responses are 403. No error state = blank UI.

CORS: the error you will hit first

Calling https://api.github.com directly from localhost works because GitHub sets Access-Control-Allow-Origin: *. But if you proxy through your own backend (e.g. to add auth tokens), and your backend does not set CORS headers: "Access to fetch at 'http://localhost:3001/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy" Fix: add cors() middleware to your Express server, or use a Next.js API route as the proxy.

Production-ready GitHub search: the full pattern

Four patterns separate a demo from a merge-ready component:

The four patterns code reviewers look for

  • Debounce the input — useDebounce hook or lodash.debounce: wait 300ms after the last keystroke before firing fetch. 26 requests becomes 1.
  • AbortController in useEffect cleanup — Return () => controller.abort() from useEffect. Stale requests are cancelled automatically. Race condition eliminated.
  • Status state machine: idle | loading | success | error — Single status string instead of three booleans (isLoading, isError, isSuccess). Impossible to be in loading AND error simultaneously.
  • Test user behaviour, not component internals — RTL: render, userEvent.type into input, assert on text the user sees. Never assert on component state or implementation details.

The code review checklist (what your senior engineer will verify)

[] Debounce: Network tab shows 1 request for "torvalds", not 8 [] AbortController: typing fast and clearing shows no stale results [] Status machine: loading spinner visible, error message is human-readable (not raw JSON) [] Empty query: no request fires for "" [] RTL test: searches for "torvalds", asserts avatar src and name appear [] Lighthouse performance: 90+ (no unoptimised avatars, lazy-load below fold)

How to present this in your interview

The interviewer will open your code. These are the questions that separate candidates who copied a tutorial from candidates who understand what they built.

Questions you will be asked — strong vs weak answers

  • How did you handle race conditions? — Strong: "AbortController in the useEffect cleanup — when the query changes, the previous fetch is cancelled before the next one fires. I verified this in the Network tab by typing quickly." Weak: "I used async/await so it is not async anymore."
  • Why debounce instead of throttle? — Strong: "Debounce fires once after the user stops typing — perfect for search. Throttle fires at a fixed interval regardless of pauses — better for scroll events or resize. I want one request per completed word, not one per 300ms." Weak: "Debounce is better for inputs."
  • Your tests pass but they only check the loading state — why is that a problem? — Strong: "Testing implementation details means the tests break when I refactor even if the behaviour is identical. I rewrote them to test what the user sees: type a username, assert the name and avatar appear. Now I can refactor the component completely and the tests still verify the feature works." Weak: "I did not know that was a problem."
  • What would change if GitHub rate-limited you at 10 requests/hour? — Strong: "I would add a token to the request header (stored server-side, never client-side), increase the debounce to 1000ms, and add a cache layer so repeated searches for the same username skip the API call entirely." Weak: "I would show an error message."
GitHubSearch.tsx (production-ready)
1// Production-ready version — passes code review
2import { useState, useEffect, useCallback } from 'react';
3
4type Status = 'idle' | 'loading' | 'success' | 'error';
5
6function useDebounce<T>(value: T, delay: number): T {
Custom useDebounce hook: reusable, testable, 300ms wait before firing fetch
7 const [debouncedValue, setDebouncedValue] = useState(value);
8 useEffect(() => {
9 const timer = setTimeout(() => setDebouncedValue(value), delay);
10 return () => clearTimeout(timer);
11 }, [value, delay]);
12 return debouncedValue;
13}
14
Status state machine: one string, four states — impossible to have loading AND error simultaneously
15function GitHubSearch() {
16 const [query, setQuery] = useState('');
17 const [user, setUser] = useState<GitHubUser | null>(null);
18 const [status, setStatus] = useState<Status>('idle');
19 const debouncedQuery = useDebounce(query, 300);
20
21 useEffect(() => {
22 if (!debouncedQuery.trim()) { setStatus('idle'); return; }
AbortController: passed as signal to fetch, cancelled in cleanup — race condition eliminated
23 const controller = new AbortController();
24 setStatus('loading');
25
26 fetch(`https://api.github.com/users/${debouncedQuery}`, {
27 signal: controller.signal,
28 })
29 .then(r => r.ok ? r.json() : Promise.reject(new Error(`User not found (status ${r.status})`)))
30 .then(data => { setUser(data); setStatus('success'); })
31 .catch(err => {
32 if (err.name !== 'AbortError') setStatus('error');
33 });
34
AbortError filtered: expected error from cleanup, not a real failure to show the user
35 return () => controller.abort(); // cancel stale request on next render
36 }, [debouncedQuery]);
37
38 return (
39 <main>
40 <input
41 aria-label="Search GitHub users"
42 value={query}
43 onChange={e => setQuery(e.target.value)}
44 placeholder="Type a GitHub username..."
role="status" and role="alert": screen readers announce loading and error states automatically
45 />
46 {status === 'loading' && <p role="status">Searching...</p>}
47 {status === 'error' && <p role="alert">User not found. Check the username.</p>}
48 {status === 'success' && user && (
49 <article aria-label={`${user.login} GitHub profile`}>
50 <img src={user.avatar_url} alt={`${user.login} avatar`} width="80" height="80" />
51 <h2>{user.name ?? user.login}</h2>
52 <p>{user.bio}</p>
53 </article>
54 )}
55 </main>
56 );
57}

Exam Answer vs. Production Reality

1 / 3

Race conditions in async React

📖 What the exam expects

A race condition occurs when two async operations run in parallel and the result depends on which finishes first. In React, this happens when a useEffect fires before the previous one completes.

Toggle between what certifications teach and what production actually requires

How this might come up in interviews

Live coding interviews frequently use "build a search-as-you-type" as the problem. Take-home tests often use a public API. The patterns here (debounce, AbortController, status machine, RTL) are exactly what interviewers probe for.

Common questions:

  • Walk me through how you would handle a race condition in a search input.
  • What is the difference between debounce and throttle? When do you use each?
  • How do you test async React components?
  • Your search fires 60 requests per hour — GitHub blocks you. What do you do?
  • How would you cache results so the same username does not re-fetch?

Strong answer: Mentions AbortController before being asked. Uses a status state machine instead of multiple booleans. Tests are written from the user perspective. Asks about rate limits and caching before building.

Red flags: No debounce on the input. No error state. Tests that check component state instead of user-visible output. Cannot explain what AbortController does.

Ready to see how this works in the cloud?

Switch to Career Paths for structured paths (e.g. Developer, DevOps) and provider-specific lessons.

View role-based paths

Discussion

Questions? Discuss in the community or start a thread below.

Join Discord

In-app Q&A

Sign in to start or join a thread.

Sign in to track your progress and mark lessons complete.

Discussion

Questions? Discuss in the community or start a thread below.

Join Discord

In-app Q&A

Sign in to start or join a thread.