Common Interview Coding Challenges
The Five Components Every Interviewer Asks You to Build
LinkedIn Hook
"Build me a todo app." Five words that have ended more React interviews than any algorithm question.
It sounds simple. But the interviewer is not checking if you can render a list. They are watching HOW you manage state, how you handle CRUD operations without mutating arrays, how you implement filtering without duplicating data, and whether you separate concerns or dump everything into one component.
Then comes round two: "Add a debounced search input." "Build a modal that traps focus." "Implement infinite scroll." "Add undo/redo to this counter." Each challenge targets a specific React skill — state management, refs, effects, custom hooks, performance.
I have seen candidates who can explain React fiber architecture in perfect detail but freeze when asked to build a filtered list from scratch. Theory without practice is a liability in live coding rounds.
This lesson walks through the five most common React coding challenges interviewers give: a todo app with full CRUD and filtering, a search input with debounce, a modal/dialog component, infinite scroll, and a counter with undo/redo. Each one is broken down step by step with the exact patterns interviewers expect to see.
These are not toy examples. These are the components that separate "I know React" from "I can build with React." Master these five, and you will handle any live coding challenge an interviewer throws at you.
Read the full lesson -> [link]
#React #JavaScript #InterviewPrep #Frontend #CodingInterview #LiveCoding #TodoApp #CustomHooks #WebDevelopment #100DaysOfCode
What You'll Learn
- How to build a complete todo app with add, edit, delete, and filter functionality using proper immutable state updates
- How to implement a debounced search input that stays responsive while delaying expensive operations
- How to build an accessible modal/dialog component with portal rendering and focus trapping
- How to implement infinite scroll using Intersection Observer and pagination state
- How to add undo/redo functionality to a counter using a history stack pattern
The Concept — Interview Coding Is a Performance, Not a Test
Analogy: The Chef's Tasting Menu
Imagine a chef auditioning for a head chef position at a restaurant. The owner does not hand them a 500-page cookbook and ask trivia questions. Instead, the owner says: "Make me five dishes. One appetizer, one salad, one soup, one main, one dessert." Each dish tests a different skill — knife work, timing, seasoning, presentation, creativity.
React interview coding challenges work the same way. Each challenge is a "dish" that tests a specific set of skills:
- Todo app = your knife work. Can you handle state, CRUD, and filtering cleanly? This is the fundamental skill.
- Debounced search = your timing. Can you control when effects fire and prevent wasted work?
- Modal/dialog = your presentation. Can you work with portals, refs, keyboard events, and layered UI?
- Infinite scroll = your seasoning. Can you combine Intersection Observer, pagination, and loading states without overcomplicating things?
- Counter with undo/redo = your creativity. Can you think beyond simple state and implement a history data structure?
The interviewer already knows these problems have solutions. They want to watch you cook — how you break the problem down, what you build first, how you name things, and whether the code is clean when you are done.
Challenge 1: Todo App — State, CRUD, and Filtering
The todo app is the "Hello World" of React interviews, but interviewers expect more than push and map. They want immutable updates, unique IDs, edit mode, and filtering without duplicating the data source.
Code Example 1: Complete Todo App with CRUD and Filtering
import { useState, useRef } from "react";
// Generate a simple unique ID without external libraries
let nextId = 0;
function generateId() {
return nextId++;
}
function TodoApp() {
// Single source of truth — all todos live here
const [todos, setTodos] = useState([]);
// Track which filter is active: "all", "active", or "completed"
const [filter, setFilter] = useState("all");
// Track which todo is being edited (null means none)
const [editingId, setEditingId] = useState(null);
// Ref for the input field so we can clear and focus it after adding
const inputRef = useRef(null);
// CREATE — add a new todo
function handleAdd(e) {
e.preventDefault();
const text = inputRef.current.value.trim();
if (!text) return;
// Spread the old array and append a new object — never mutate
setTodos((prev) => [
...prev,
{ id: generateId(), text, completed: false },
]);
inputRef.current.value = "";
inputRef.current.focus();
}
// UPDATE — toggle the completed status
function handleToggle(id) {
// Map creates a new array, and we create a new object for the changed item
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}
// UPDATE — edit the text of a todo
function handleEdit(id, newText) {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, text: newText } : todo
)
);
setEditingId(null);
}
// DELETE — remove a todo by filtering it out
function handleDelete(id) {
// Filter returns a new array without the deleted item
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}
// FILTER — derive the visible list from the single source of truth
// This is a computed value, NOT separate state
const filteredTodos = todos.filter((todo) => {
if (filter === "active") return !todo.completed;
if (filter === "completed") return todo.completed;
return true; // "all"
});
return (
<div>
{/* Add form */}
<form onSubmit={handleAdd}>
<input ref={inputRef} placeholder="What needs to be done?" />
<button type="submit">Add</button>
</form>
{/* Filter buttons */}
<div>
{["all", "active", "completed"].map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
style={{ fontWeight: filter === f ? "bold" : "normal" }}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
</button>
))}
</div>
{/* Todo list */}
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
{editingId === todo.id ? (
// Edit mode — show an input pre-filled with current text
<input
defaultValue={todo.text}
autoFocus
onBlur={(e) => handleEdit(todo.id, e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleEdit(todo.id, e.target.value);
if (e.key === "Escape") setEditingId(null);
}}
/>
) : (
// Display mode — double-click to enter edit mode
<span
onDoubleClick={() => setEditingId(todo.id)}
style={{
textDecoration: todo.completed ? "line-through" : "none",
}}
>
{todo.text}
</span>
)}
<button onClick={() => handleDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
{/* Item count */}
<p>{todos.filter((t) => !t.completed).length} items left</p>
</div>
);
}
// Output after adding "Learn React", "Build projects", toggling "Learn React":
//
// [x] Learn React [Delete]
// [ ] Build projects [Delete]
//
// Filter "Active" -> shows only "Build projects"
// Filter "Completed" -> shows only "Learn React"
// Filter "All" -> shows both
// 1 items left
Key point: The filtered list is derived from the main todos array using .filter() — it is NOT stored as separate state. This is a critical pattern interviewers look for. Storing filtered results in a separate useState creates sync bugs. Derive, do not duplicate.
Challenge 2: Search Input with Debounce
Interviewers use this to test your understanding of effects, cleanup, and performance. The trap is debouncing the handler instead of the value, which makes the input feel laggy.
Code Example 2: Debounced Search with Results Display
import { useState, useEffect, useRef } from "react";
// Reusable debounce hook — the interviewer expects you to write this from memory
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Set a timer to update the debounced value after the delay
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cleanup: clear the old timer when value changes or component unmounts
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Simulated API function for demonstration
function fakeSearch(query) {
const allItems = [
"React Hooks", "React Router", "React Query",
"Redux Toolkit", "JavaScript Closures", "TypeScript Generics",
];
return new Promise((resolve) => {
setTimeout(() => {
resolve(
allItems.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
)
);
}, 500);
});
}
function DebouncedSearch() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
// The input updates instantly, but this value only changes after 300ms of silence
const debouncedQuery = useDebounce(query, 300);
// Ref to track the latest request and ignore stale responses
const latestRequestRef = useRef(0);
useEffect(() => {
if (!debouncedQuery.trim()) {
setResults([]);
return;
}
// Increment the request counter to track which request is "current"
const requestId = ++latestRequestRef.current;
setIsLoading(true);
fakeSearch(debouncedQuery).then((data) => {
// Only update state if this is still the latest request
// A newer request may have started while this one was in flight
if (requestId === latestRequestRef.current) {
setResults(data);
setIsLoading(false);
}
});
}, [debouncedQuery]);
return (
<div>
{/* The input is controlled and updates on every keystroke — no lag */}
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search topics..."
/>
{isLoading && <p>Searching...</p>}
<ul>
{results.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
{!isLoading && debouncedQuery && results.length === 0 && (
<p>No results for "{debouncedQuery}"</p>
)}
</div>
);
}
// User types "react" quickly:
// Keystrokes: r -> re -> rea -> reac -> react (5 onChange events)
// After 300ms of silence, ONE search fires for "react"
// Output:
// React Hooks
// React Router
// React Query
//
// Without debounce: 5 separate API calls for "r", "re", "rea", "reac", "react"
// With debounce: 1 API call for "react"
Key point: The input stays responsive because query updates on every keystroke. The expensive operation (search) only fires when debouncedQuery changes — which is 300ms after the user stops typing. The latestRequestRef pattern prevents stale responses from overwriting newer results. In a production app, you would use AbortController instead of a request counter.
Challenge 3: Modal/Dialog Component
This challenge tests portals, keyboard handling, focus management, and click-outside detection. Interviewers want to see that you think about accessibility, not just visual behavior.
Code Example 3: Accessible Modal with Portal and Focus Trap
import { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
function Modal({ isOpen, onClose, title, children }) {
const modalRef = useRef(null);
const previousActiveElement = useRef(null);
useEffect(() => {
if (!isOpen) return;
// Save the element that was focused before the modal opened
// We will restore focus to it when the modal closes
previousActiveElement.current = document.activeElement;
// Focus the modal container so keyboard events work immediately
modalRef.current?.focus();
// Close the modal when the user presses Escape
function handleKeyDown(e) {
if (e.key === "Escape") {
onClose();
}
}
// Prevent scrolling the page behind the modal
document.body.style.overflow = "hidden";
document.addEventListener("keydown", handleKeyDown);
return () => {
// Restore scroll and remove the keyboard listener on cleanup
document.body.style.overflow = "";
document.removeEventListener("keydown", handleKeyDown);
// Return focus to the element that was focused before the modal opened
previousActiveElement.current?.focus();
};
}, [isOpen, onClose]);
// Close when the user clicks the backdrop (the dark overlay behind the modal)
function handleBackdropClick(e) {
// Only close if the click was on the backdrop itself, not inside the modal
if (e.target === e.currentTarget) {
onClose();
}
}
if (!isOpen) return null;
// Render the modal into a portal so it sits above everything in the DOM
return createPortal(
<div
onClick={handleBackdropClick}
style={{
position: "fixed",
inset: 0,
backgroundColor: "rgba(0, 0, 0, 0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
}}
role="dialog"
aria-modal="true"
aria-label={title}
>
<div
ref={modalRef}
tabIndex={-1}
style={{
background: "white",
borderRadius: 8,
padding: 24,
minWidth: 300,
maxWidth: 500,
}}
>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<h2>{title}</h2>
<button onClick={onClose} aria-label="Close modal">
X
</button>
</div>
<div>{children}</div>
</div>
</div>,
document.body
);
}
// Usage
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<h1>My Application</h1>
<button onClick={() => setShowModal(true)}>Open Settings</button>
<Modal
isOpen={showModal}
onClose={() => setShowModal(false)}
title="Settings"
>
<p>Configure your preferences here.</p>
<input placeholder="Username" />
<button onClick={() => setShowModal(false)}>Save</button>
</Modal>
</div>
);
}
// Output:
// Clicking "Open Settings" -> dark overlay appears, white modal in center
// Pressing Escape -> modal closes
// Clicking the dark overlay -> modal closes
// Clicking inside the modal -> modal stays open
// Focus returns to the "Open Settings" button after the modal closes
Key point: The three things interviewers watch for are: (1) using createPortal so the modal renders at the body level and avoids z-index and overflow issues, (2) handling Escape key and click-outside to close, and (3) restoring focus to the previously focused element when the modal closes. Accessibility is not bonus points — it is expected.
Challenge 4: Infinite Scroll with Intersection Observer
This tests your ability to combine refs, effects, and pagination state. The modern approach uses IntersectionObserver instead of scroll event listeners.
Code Example 4: Infinite Scroll with Intersection Observer
import { useState, useEffect, useRef, useCallback } from "react";
// Simulated API that returns paginated data
function fetchItems(page) {
return new Promise((resolve) => {
setTimeout(() => {
const items = Array.from({ length: 10 }, (_, i) => ({
id: page * 10 + i,
title: `Item ${page * 10 + i + 1}`,
}));
// Simulate a total of 50 items (5 pages)
const hasMore = page < 4;
resolve({ items, hasMore });
}, 800);
});
}
function InfiniteScrollList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
// Ref for the "sentinel" element at the bottom of the list
// When this element becomes visible, we load the next page
const sentinelRef = useRef(null);
// Fetch data for the current page
const loadMore = useCallback(async () => {
if (isLoading || !hasMore) return;
setIsLoading(true);
const data = await fetchItems(page);
// Append new items to the existing list — do NOT replace
setItems((prev) => [...prev, ...data.items]);
setHasMore(data.hasMore);
setPage((prev) => prev + 1);
setIsLoading(false);
}, [page, isLoading, hasMore]);
// Set up the Intersection Observer to watch the sentinel element
useEffect(() => {
const sentinel = sentinelRef.current;
if (!sentinel) return;
const observer = new IntersectionObserver(
(entries) => {
// When the sentinel enters the viewport, load the next page
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 0.1 } // Trigger when 10% of the sentinel is visible
);
observer.observe(sentinel);
// Cleanup: stop observing when the component unmounts or deps change
return () => observer.disconnect();
}, [loadMore]);
return (
<div style={{ maxHeight: 400, overflow: "auto" }}>
<ul>
{items.map((item) => (
<li key={item.id} style={{ padding: "16px", borderBottom: "1px solid #eee" }}>
{item.title}
</li>
))}
</ul>
{/* Sentinel element — invisible, sits at the bottom of the list */}
{hasMore && (
<div ref={sentinelRef} style={{ padding: 16, textAlign: "center" }}>
{isLoading ? "Loading more..." : "Scroll for more"}
</div>
)}
{!hasMore && <p style={{ textAlign: "center" }}>You have reached the end.</p>}
</div>
);
}
// Output on initial load:
// Item 1
// Item 2
// ... (10 items)
// "Scroll for more"
//
// User scrolls to bottom -> sentinel enters viewport -> next 10 items load
// After 5 pages (50 items): "You have reached the end."
Key point: IntersectionObserver is far more performant than attaching a scroll event listener. It runs off the main thread and does not cause layout thrashing. The sentinel pattern — placing an invisible element at the bottom and observing it — is the standard approach used by libraries like react-infinite-scroll-component and react-virtuoso. Interviewers specifically look for this over the older onScroll approach.
Challenge 5: Counter with Undo/Redo
This challenge tests whether you can think beyond basic state. The undo/redo pattern requires a history stack — an array of past states and a pointer to the current position.
Code Example 5: Counter with Full Undo/Redo History
import { useReducer, useCallback } from "react";
// The history state has three parts:
// past: array of previous counter values
// present: the current counter value
// future: array of values that were undone (available for redo)
const initialState = {
past: [],
present: 0,
future: [],
};
function undoReducer(state, action) {
const { past, present, future } = state;
switch (action.type) {
case "SET": {
// A new action wipes out the future — you cannot redo after making a new change
return {
past: [...past, present],
present: action.payload,
future: [],
};
}
case "UNDO": {
// Move the current value into the future, pop the last past value
if (past.length === 0) return state;
const previous = past[past.length - 1];
return {
past: past.slice(0, -1),
present: previous,
future: [present, ...future],
};
}
case "REDO": {
// Move the current value into the past, pop the first future value
if (future.length === 0) return state;
const next = future[0];
return {
past: [...past, present],
present: next,
future: future.slice(1),
};
}
case "RESET": {
return initialState;
}
default:
return state;
}
}
function CounterWithUndoRedo() {
const [state, dispatch] = useReducer(undoReducer, initialState);
const { past, present, future } = state;
// Wrap counter operations to go through the history system
const increment = useCallback(() => {
dispatch({ type: "SET", payload: present + 1 });
}, [present]);
const decrement = useCallback(() => {
dispatch({ type: "SET", payload: present - 1 });
}, [present]);
const undo = useCallback(() => dispatch({ type: "UNDO" }), []);
const redo = useCallback(() => dispatch({ type: "REDO" }), []);
const reset = useCallback(() => dispatch({ type: "RESET" }), []);
return (
<div>
<h2>Count: {present}</h2>
<div>
<button onClick={decrement}>-1</button>
<button onClick={increment}>+1</button>
</div>
<div>
{/* Disable undo when there is no history to go back to */}
<button onClick={undo} disabled={past.length === 0}>
Undo ({past.length})
</button>
{/* Disable redo when there is no future to go forward to */}
<button onClick={redo} disabled={future.length === 0}>
Redo ({future.length})
</button>
<button onClick={reset}>Reset</button>
</div>
{/* Show the history for debugging and interview demonstration */}
<p>History: [{past.join(", ")}] | {present} | [{future.join(", ")}]</p>
</div>
);
}
// User clicks: +1, +1, +1, Undo, Undo, Redo
//
// Step 1 (+1): past: [0], present: 1, future: [] -> "Count: 1"
// Step 2 (+1): past: [0,1], present: 2, future: [] -> "Count: 2"
// Step 3 (+1): past: [0,1,2], present: 3, future: [] -> "Count: 3"
// Step 4 (Undo): past: [0,1], present: 2, future: [3] -> "Count: 2"
// Step 5 (Undo): past: [0], present: 1, future: [2,3] -> "Count: 1"
// Step 6 (Redo): past: [0,1], present: 2, future: [3] -> "Count: 2"
//
// History display: [0, 1] | 2 | [3]
Key point: The undo/redo pattern uses three arrays: past, present, and future. Every new action pushes the current value to past and clears future. Undo moves present to future and pops from past. Redo does the reverse. This exact pattern is used by Redux Undo, Zustand middleware, and text editors. The interviewer is testing whether you understand data structure design, not just React APIs.
Common Mistakes
Mistake 1: Storing filtered data as separate state instead of deriving it
// BAD: Two sources of truth that can get out of sync
function TodoApp() {
const [todos, setTodos] = useState([]);
const [filteredTodos, setFilteredTodos] = useState([]); // WRONG
const [filter, setFilter] = useState("all");
// Now you have to manually sync filteredTodos whenever todos OR filter changes
// Miss one place and the UI shows stale data
function handleDelete(id) {
const updated = todos.filter((t) => t.id !== id);
setTodos(updated);
// Forgot to also update filteredTodos — BUG: deleted item still shows
setFilteredTodos(updated.filter(/* ... */));
}
}
// GOOD: Single source of truth, derive the filtered list on every render
function TodoApp() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState("all");
// Computed from existing state — always in sync, impossible to get stale
const filteredTodos = todos.filter((todo) => {
if (filter === "active") return !todo.completed;
if (filter === "completed") return todo.completed;
return true;
});
// Delete only needs to update one piece of state
function handleDelete(id) {
setTodos((prev) => prev.filter((t) => t.id !== id));
// filteredTodos automatically updates on the next render
}
}
// RULE: If you can compute it from existing state, do NOT store it in state.
Mistake 2: Mutating state directly in CRUD operations
// BAD: Direct mutation — React will not detect the change and will not re-render
function handleToggle(id) {
const todo = todos.find((t) => t.id === id);
todo.completed = !todo.completed; // Mutation — same object reference
setTodos(todos); // Same array reference — React skips the re-render
}
// BAD: Using push instead of spread — mutates the original array
function handleAdd(text) {
todos.push({ id: Date.now(), text, completed: false }); // Mutation
setTodos(todos); // Same reference — no re-render
}
// GOOD: Create new arrays and new objects for every state update
function handleToggle(id) {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}
function handleAdd(text) {
setTodos((prev) => [
...prev,
{ id: Date.now(), text, completed: false },
]);
}
// Use .map() for updates, .filter() for deletes, [...spread] for adds.
// Never use .push(), .splice(), or direct property assignment on state.
Mistake 3: Not cleaning up Intersection Observer or event listeners
// BAD: Observer is never disconnected — leaks memory and fires callbacks
// on elements that are no longer relevant
function InfiniteScroll() {
const sentinelRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) loadMore();
});
observer.observe(sentinelRef.current);
// Missing cleanup — observer persists after unmount
}, []);
}
// GOOD: Always disconnect the observer in the useEffect cleanup function
function InfiniteScroll() {
const sentinelRef = useRef(null);
useEffect(() => {
const sentinel = sentinelRef.current;
if (!sentinel) return;
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) loadMore();
});
observer.observe(sentinel);
// Cleanup: disconnect when the component unmounts or dependencies change
return () => observer.disconnect();
}, [loadMore]);
}
// The same rule applies to addEventListener — every add needs a matching remove.
// useEffect cleanup is the mechanism React provides for this.
Interview Questions
Q: Walk me through how you would build a todo app from scratch. What state do you need?
I need three pieces of state: the
todosarray (each item has anid,text, andcompletedboolean), the currentfiltervalue ("all", "active", or "completed"), and optionally aneditingIdto track which todo is in edit mode. The filtered list is NOT separate state — it is derived by calling.filter()on the todos array using the current filter value. For CRUD operations, I use.map()with spread for updates,.filter()for deletes, and spread with a new object for creates. All updates are immutable — I never mutate the existing array or objects.
Q: Why do you derive the filtered list instead of storing it as state?
Storing derived data as separate state creates two sources of truth that can get out of sync. If I store both
todosandfilteredTodos, every operation that changestodosalso needs to manually updatefilteredTodos. Miss one place, and the UI shows stale data. By computingfilteredTodosfromtodosandfilteron each render, it is always correct — impossible to have a sync bug. If the computation is expensive, I can memoize it withuseMemo, but the source of truth remains a singletodosarray.
Q: How would you implement debounce in React without any library?
I would create a
useDebouncecustom hook. It takes a value and a delay. Inside, it usesuseStateto hold the debounced value and auseEffectthat sets asetTimeoutto update the debounced value after the delay. The key is the cleanup function — it callsclearTimeouton the previous timer whenever the value changes. This means each keystroke resets the timer, and the debounced value only updates after the user stops typing for the specified delay. I use the debounced value to trigger the expensive operation (like a fetch), while the raw value drives the controlled input so it stays responsive.
Q: What are the three essential behaviors an accessible modal needs?
First, it must render through a portal (
createPortal) to the body element so it sits above all other content and avoids overflow or z-index issues from parent containers. Second, it must handle keyboard interactions — Escape key to close, and ideally trap Tab focus within the modal so the user cannot tab to elements behind the overlay. Third, it must manage focus — when the modal opens, focus moves to the modal or its first interactive element, and when it closes, focus returns to the element that triggered the modal. It should also haverole="dialog",aria-modal="true", and anaria-labelfor screen readers.
Q: Explain the undo/redo data structure. How does it work?
The state has three parts:
past(an array of previous values),present(the current value), andfuture(an array of undone values). When the user performs an action, the currentpresentis pushed topast, the new value becomespresent, andfutureis cleared — because making a new change after undoing invalidates the redo history. Undo pops the last value frompastintopresentand pushes the oldpresentto the front offuture. Redo does the reverse — pops the first value fromfutureintopresentand pushes the oldpresentto the end ofpast. This is the same data structure used by Redux Undo and most text editor implementations.
Quick Reference -- Cheat Sheet
FIVE INTERVIEW CODING CHALLENGES
============================================================
Challenge 1: Todo App (State, CRUD, Filtering)
State needed: todos[], filter, editingId
Create: setTodos(prev => [...prev, newTodo])
Read: filteredTodos = todos.filter(...) // DERIVED, not stored
Update: setTodos(prev => prev.map(t => t.id === id ? {...t, changes} : t))
Delete: setTodos(prev => prev.filter(t => t.id !== id))
Key rule: Never mutate. Use map/filter/spread for every update.
Challenge 2: Debounced Search
Hook: useDebounce(value, delay) -> useState + useEffect + setTimeout
Cleanup: return () => clearTimeout(timer) in useEffect
Input: Controlled, updates on every keystroke (stays responsive)
Search: Fires only when debouncedValue changes
Race fix: AbortController or request ID ref to ignore stale responses
Challenge 3: Modal/Dialog
Portal: createPortal(modalJSX, document.body)
Close on: Escape key + backdrop click (e.target === e.currentTarget)
Focus: Save previous focus -> focus modal -> restore on close
Scroll lock: document.body.style.overflow = "hidden"
ARIA: role="dialog", aria-modal="true", aria-label={title}
Challenge 4: Infinite Scroll
Observer: new IntersectionObserver(callback, { threshold: 0.1 })
Sentinel: Invisible div at bottom of list, observed by the observer
State: items[] (append, never replace), page, isLoading, hasMore
Append: setItems(prev => [...prev, ...newItems])
Cleanup: return () => observer.disconnect() in useEffect
Challenge 5: Undo/Redo Counter
Structure: { past: [], present: value, future: [] }
New action: past = [...past, present], present = new, future = []
Undo: present -> future, past.pop() -> present
Redo: present -> past, future.shift() -> present
Key rule: New actions clear future (no redo after new change)
GENERAL INTERVIEW TIPS:
1. Talk as you code — explain your decisions out loud
2. Start with state design before writing JSX
3. Use immutable patterns for every state update
4. Derive computed values instead of storing them
5. Clean up effects (timers, observers, listeners)
Previous: Lesson 10.2 -- Testing Hooks & Async Code -> Next: Lesson 10.4 -- React Interview Questions -- What Interviewers Actually Ask ->
This is Lesson 10.3 of the React Interview Prep Course -- 10 chapters, 42 lessons.