Build it live, build it right
Machine-coding rounds reward clear state, immutable updates, cleanup of timers and requests, and accessibility. Talk through the state shape before typing.
- StateTodo, accordion, tabs and rating.
- EffectsDebounce, timers and infinite scroll.
- AccessibilityLabels, ARIA and native dialog.
Machine-coding rounds give you 45–90 minutes to build a working component. Interviewers judge state design, clean components, accessibility and edge cases. Each solution below is short enough to write live and complete enough to impress.
1. Build a todo list with add, toggle and delete.
One array of objects in state, immutable updates, stable ids as keys, and an accessible form.
function TodoApp() {
const [todos, setTodos] = useState([]);
const [text, setText] = useState('');
function add(e) {
e.preventDefault();
if (!text.trim()) return;
setTodos(t => [...t, { id: crypto.randomUUID(), title: text.trim(), done: false }]);
setText('');
}
const toggle = id => setTodos(t => t.map(x => (x.id === id ? { ...x, done: !x.done } : x)));
const remove = id => setTodos(t => t.filter(x => x.id !== id));
return (
<form onSubmit={add}>
<label htmlFor="new-todo">New todo</label>
<input id="new-todo" value={text} onChange={e => setText(e.target.value)} />
<button>Add</button>
<ul>
{todos.map(t => (
<li key={t.id}>
<input type="checkbox" checked={t.done} onChange={() => toggle(t.id)} aria-label={`Mark ${t.title} done`} />
<span style={{ textDecoration: t.done ? 'line-through' : 'none' }}>{t.title}</span>
<button type="button" onClick={() => remove(t.id)}>Delete</button>
</li>
))}
</ul>
</form>
);
}2. Build a debounced search that ignores stale responses.
Debounce the query with an effect timer, then fetch with an AbortController so an older, slower response cannot overwrite a newer one.
function useDebounced(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const q = useDebounced(query);
useEffect(() => {
if (!q) { setResults([]); return; }
const controller = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal })
.then(r => r.json()).then(setResults)
.catch(e => { if (e.name !== 'AbortError') console.error(e); });
return () => controller.abort();
}, [q]);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search courses" />
<ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
</>
);
}3. Build an accessible star rating component.
Controlled value from the parent, hover preview, keyboard support through native radio inputs.
function StarRating({ value, onChange, max = 5 }) {
const [hover, setHover] = useState(0);
return (
<div role="radiogroup" aria-label="Rating" onMouseLeave={() => setHover(0)}>
{Array.from({ length: max }, (_, i) => i + 1).map(n => (
<label key={n} onMouseEnter={() => setHover(n)}>
<input type="radio" name="rating" value={n} checked={value === n} onChange={() => onChange(n)} className="sr-only" />
<span aria-hidden="true">{n <= (hover || value) ? '★' : '☆'}</span>
<span className="sr-only">{n} star{n > 1 ? 's' : ''}</span>
</label>
))}
</div>
);
}4. Build an accordion where only one panel is open.
Store the open index in the parent; use buttons with aria-expanded and aria-controls.
function Accordion({ items }) {
const [open, setOpen] = useState(null);
return items.map((item, i) => (
<section key={item.id}>
<h3>
<button aria-expanded={open === i} aria-controls={`panel-${item.id}`} onClick={() => setOpen(open === i ? null : i)}>
{item.title}
</button>
</h3>
<div id={`panel-${item.id}`} hidden={open !== i}>{item.body}</div>
</section>
));
}5. Build a countdown timer with start, pause and reset.
Keep the remaining seconds in state and the interval id in a ref; clear the interval on pause, at zero and on unmount.
function Countdown({ seconds = 60 }) {
const [left, setLeft] = useState(seconds);
const [running, setRunning] = useState(false);
useEffect(() => {
if (!running) return;
const id = setInterval(() => {
setLeft(s => {
if (s <= 1) { setRunning(false); return 0; }
return s - 1;
});
}, 1000);
return () => clearInterval(id); // pause and unmount clean-up
}, [running]);
const mm = String(Math.floor(left / 60)).padStart(2, '0');
const ss = String(left % 60).padStart(2, '0');
return (
<div>
<p aria-live="polite">{mm}:{ss}</p>
<button onClick={() => setRunning(r => !r)}>{running ? 'Pause' : 'Start'}</button>
<button onClick={() => { setRunning(false); setLeft(seconds); }}>Reset</button>
</div>
);
}6. Build tabs with keyboard support.
Selected index in state; arrow keys move between tabs, following the WAI-ARIA tabs pattern.
function Tabs({ tabs }) {
const [active, setActive] = useState(0);
const onKeyDown = e => {
if (e.key === 'ArrowRight') setActive(a => (a + 1) % tabs.length);
if (e.key === 'ArrowLeft') setActive(a => (a - 1 + tabs.length) % tabs.length);
};
return (
<>
<div role="tablist" onKeyDown={onKeyDown}>
{tabs.map((t, i) => (
<button key={t.id} role="tab" aria-selected={active === i} tabIndex={active === i ? 0 : -1} onClick={() => setActive(i)}>
{t.label}
</button>
))}
</div>
<div role="tabpanel">{tabs[active].content}</div>
</>
);
}7. Build infinite scroll with IntersectionObserver.
Observe a sentinel at the bottom; when it appears, load the next page unless already loading or finished.
function Feed() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const sentinel = useRef(null);
useEffect(() => {
if (!hasMore) return;
setLoading(true);
fetch(`/api/posts?page=${page}`).then(r => r.json()).then(data => {
setItems(prev => [...prev, ...data.items]);
setHasMore(data.hasMore);
}).finally(() => setLoading(false));
}, [page, hasMore]);
useEffect(() => {
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !loading && hasMore) setPage(p => p + 1);
}, { rootMargin: '200px' });
if (sentinel.current) io.observe(sentinel.current);
return () => io.disconnect();
}, [loading, hasMore]);
return (
<>
{items.map(p => <article key={p.id}>{p.title}</article>)}
{loading && <p>Loading…</p>}
<div ref={sentinel} />
</>
);
}8. Build a reusable modal that closes on Escape and outside click.
Use the native <dialog> element for focus trapping and Escape handling, controlled by an open prop.
function Modal({ open, onClose, title, children }) {
const ref = useRef(null);
useEffect(() => {
const dialog = ref.current;
if (open && !dialog.open) dialog.showModal();
if (!open && dialog.open) dialog.close();
}, [open]);
return (
<dialog ref={ref} onClose={onClose} onClick={e => { if (e.target === ref.current) onClose(); }} aria-labelledby="modal-title">
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose}>Close</button>
</dialog>
);
}Remember
Key points
- Functional state updates
setX(prev => …) inside timers.
- Clean up every effect
Intervals, observers, requests.
- Abort stale fetches
Prevent out-of-order results.
- Native elements first
dialog, button, radio inputs.
Interview drill
Follow-up questions
What the interviewer asks after your first answer. Turn on practice mode, answer each one aloud, then reveal.
-
1 How would you persist the todo list across reloads? Basic
Initialise state lazily from localStorage and save in an effect when todos change (guarding JSON parsing errors).
-
2 How would you implement undo for delete? Intermediate
Keep the last deleted item (or a history stack) and restore it within a few seconds via a toast action.
-
3 How do you build a drag-and-drop reorder? Advanced
Use the HTML drag events or a library such as dnd-kit, storing the order in state and providing keyboard alternatives for accessibility.
-
4 How do you show a toast notification system? Intermediate
A context provider holding a list of toasts, a portal that renders them, and timeouts to remove each one.
-
5 How would you virtualise a list of 10,000 rows? Advanced
Render only visible rows with absolute positioning based on scrollTop and row height, or use TanStack Virtual.
-
6 How do you prevent double form submission? Basic
Disable the submit button while a request is in flight and ignore submits when a pending flag is set.