What frontend interviews actually test
Frontend coding interviews are not algorithm puzzles with a React logo on top. They test whether you understand how JavaScript really behaves, whether you can build a small interactive feature without fighting the framework, and whether you make sensible calls about state, rendering, and accessibility under time pressure. The same person who can reverse a linked list can still struggle to explain why a stale closure broke an event handler, and that gap is exactly what these rounds find.
There are three common formats, and companies mix them freely:
| Round | What it looks like | What is really being measured |
|---|---|---|
| Core JavaScript | Predict output, explain this, reason about the event loop | Do you understand the runtime, not just the syntax |
| Build | Implement a typeahead, star rating, or todo app live | Can you model state and async without creating bugs |
| Utility | Write debounce, an event emitter, or a promise pool from scratch | Do you know the language well enough to build its tools |
A useful mental model: the interviewer is not grading the final artefact, they are grading the decisions you make on the way there. A half-finished feature built with clear reasoning beats a complete one built by guessing. Treat every choice, even small ones like where state lives, as something to say out loud.
The single biggest predictor of a strong result is not raw speed. It is whether the interviewer can follow your thinking well enough to help you when you stall. Silence removes their ability to do that.
The signals that separate a hire from a pass
Before the techniques, it helps to know what the two ends of the spectrum sound like, because the difference is rarely about knowing more APIs.
| Signal | Weak candidate | Strong candidate |
|---|---|---|
| Starting a build | Types immediately, refactors three times | Spends 60 seconds naming the state and the events first |
| Hitting a bug | Goes quiet, adds random changes | Narrates a hypothesis, then tests it |
| Async work | Fires a fetch in an effect with no cleanup | Mentions cancellation and race conditions unprompted |
| Optimisation | Wraps everything in memo reflexively | Says "I would measure first" and explains the tradeoff |
| Edge cases | Builds only the happy path | Calls out empty, loading, and error states early |
None of this requires senior experience. It requires treating the interview as a collaboration rather than a test you have to survive.
Get the language fundamentals solid
Before any framework, the interviewer wants to know that you understand the runtime. The topics that come up again and again are closures, the event loop, this binding, and how promises schedule work.
A classic trap is the loop with var. Interviewers usually write it with setTimeout, but the real bug is scope: because var is function scoped, every closure created in the loop shares one binding and reads its final value. You can prove that without any timer by collecting the closures and calling them:
const withVar = [];
for (var i = 0; i < 3; i++) withVar.push(() => i);
console.log(withVar.map((f) => f())); // [3,3,3] - one shared i, left at 3
const withLet = [];
for (let j = 0; j < 3; j++) withLet.push(() => j);
console.log(withLet.map((f) => f())); // [0,1,2] - each iteration gets its own jSwapping var for let gives each iteration its own binding, which is why the second loop reads 0, 1, 2. The classic setTimeout(() => console.log(i), 0) version prints 3, 3, 3 for exactly this reason: the callbacks run after the loop finishes, by which point the single var i is already 3. Talking through the scope, rather than reciting the output, is what the round rewards. If the interviewer pushes further, show that you can fix the var version without let too, by capturing the value in an IIFE or by passing it as a third argument to setTimeout.
Microtasks versus macrotasks is the next layer up. When promises and timers are mixed, the order is not obvious, and interviewers love to probe it. The canonical question mixes a console.log, a setTimeout(..., 0), and a Promise.resolve().then(...), and the answer is start, end, promise, timeout. The mechanism is easier to hold onto if you model the three queues explicitly:
const output = [];
const microtasks = []; // Promise.resolve().then(...) lands here
const macrotasks = []; // setTimeout(..., 0) lands here
output.push("start");
macrotasks.push(() => output.push("timeout"));
microtasks.push(() => output.push("promise"));
output.push("end");
while (microtasks.length) microtasks.shift()(); // drain microtasks first
while (macrotasks.length) macrotasks.shift()(); // then the next macrotask
console.log(output.join(", ")); // start, end, promise, timeoutThe key sentence is: synchronous code runs first, then the microtask queue drains completely (the promise callbacks), then the next macrotask runs (the timer). The MDN microtask guide puts it precisely: a microtask runs "after the function or program which created it exits and only if the JavaScript execution stack is empty, but before returning control to the event loop." If you can say that cleanly, you have answered most of what this round is checking.
this binding is the other reliable topic. Know how a plain function call, a method call, an arrow function, bind, and new each resolve this. The trap to watch for is passing a method as a callback and losing its receiver, then fixing it with an arrow wrapper or bind.
Practise the small utilities
Implementing a utility from scratch is a favourite because it is quick to set up and reveals a lot. Debounce is the most common. Practise writing it without looking, including the part where you preserve this and the arguments.
function debounce(fn, delayMs) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delayMs);
};
}
// A manual clock so we can watch a burst collapse into one call (no real timers).
let now = 0;
let scheduled = null;
const setTimeout = (fn, ms) => { scheduled = { fn, at: now + ms }; };
const clearTimeout = () => { scheduled = null; };
const tick = (ms) => { now += ms; if (scheduled && now >= scheduled.at) { const f = scheduled.fn; scheduled = null; f(); } };
let runs = 0;
const search = debounce(() => { runs++; }, 200);
search(); search(); search(); // three rapid keystrokes
tick(200); // let the clock pass the delay
console.log(runs); // 1 - only the last call in the burst runsThe follow-up almost always raises the bar. A senior version of debounce supports a leading edge (fire immediately, then suppress) and a cancel method. You do not need to write all of it under pressure, but say that you know those variations exist and when each is appropriate. Search-as-you-type usually wants trailing debounce; a "save" button that must not double-submit usually wants leading.
Other utilities worth having in your fingers:
- throttle, and be ready to explain how it differs from debounce. Throttle guarantees a call at a steady rate; debounce waits for quiet.
- a simple
EventEmitterwithon,off, andemit. Theoffmethod is where people get tripped up, so practise removing the right listener. deepClone, with an honest note thatstructuredCloneexists in modern environments and handles cycles, while a naive recursive version does not.- a promise pool that runs N promises with limited concurrency. This one separates strong candidates because it forces you to reason about scheduling.
For each, be ready to discuss edge cases out loud: what happens if the wrapped function throws, what happens on rapid repeated calls, and how you would test it. A short verbal test plan ("I would assert it only fires once after a burst, and that arguments are forwarded") is a strong signal that costs ten seconds.
Building a React feature live: a worked example
The build round is where most of the time goes. You will be asked to make something interactive, such as a typeahead search, a star rating, a paginated list, or a small todo app. Rather than list tips in the abstract, here is how a strong candidate would actually walk through a typeahead.
Step 1: Name the state and events before typing. Out loud: "I need the query string and the results array. Loading is derived from whether a request is in flight. Suggestions visible is derived from results plus a focus flag. I have two events: the user types, and the user selects a result." This 60-second narration is the highest-leverage thing you can do.
Step 2: Model the data minimally. Decide what state you truly need and keep it small. Derive everything you can rather than storing it. For a typeahead, the state is the query and the results; the loading flag and the visible suggestions fall out of those.
function useSearch(query) {
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
const controller = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
.then((r) => r.json())
.then(setResults)
.catch((err) => {
if (err.name !== "AbortError") setResults([]);
});
return () => controller.abort();
}, [query]);
return results;
}Step 3: Call out the two bugs this design avoids before you are asked. The cleanup calls AbortController.abort() on the in-flight request, so an old slow response cannot overwrite a newer one. That is the race condition most candidates ship without noticing. The empty-query branch resets results so stale suggestions do not linger. Saying "I am aborting here specifically to prevent a stale response winning a race" is the line that separates a mid-level answer from a senior one.
Step 4: Add debouncing where it belongs. The query should be debounced before it reaches the network, not the keystrokes themselves. Mention that you would debounce the value with a small hook so typing stays responsive while requests stay cheap. If you have time, wire it; if not, leave a clear comment and say what it would do.
Step 5: Handle the states a real user hits. Wire the empty, loading, error, and no-results cases. Add keyboard support: arrow keys to move through suggestions, Enter to select, Escape to close. This is where you earn the accessibility signal, covered below.
This five-step shape generalises. For a star rating, the state is the selected value and a hovered value; the rest is derived. For a paginated table, the state is the page number and page size; the rows are fetched from those. Naming the minimal state first, then deriving, is the move that makes the whole round calmer.
Show that you understand rendering
Interviewers often probe whether you know why a component re-renders and when that matters. You do not need to reach for memoisation everywhere, and saying so is itself a good answer. Reach for it when you have measured a real cost.
Be ready to explain the dependency array of useEffect, why an object or function in that array can cause an effect to run every render, and how useCallback or useMemo stabilise those references. The trap is creating a new object literal or inline function and passing it as a dependency, which silently defeats the comparison and re-runs the effect on every render.
Be honest that premature optimisation with memo can make code harder to read for no benefit, and can even cost more than it saves once you add the comparison work and the stabilising hooks around it. The senior signal is knowing the tradeoff, not reciting the API. A clean answer sounds like: "I would reach for useMemo here only if this list got large enough to measure a delay; below that it is noise." The React useMemo reference makes the same call: memoisation is only worth adding when a calculation is "noticeably slow" or feeds a memo-wrapped child, and otherwise it just costs readability.
Keys in lists come up constantly. Explain why using an array index as a key breaks when items are inserted or reordered, because React reuses the wrong DOM node and component state attaches to the wrong row, and why a stable id fixes it. The React docs on rendering lists are blunt that "index as a key often leads to subtle and confusing bugs." A concrete example, like an input inside each row keeping the wrong value after a delete, makes the point land.
Do not forget accessibility and the small details
Frontend rounds reward candidates who remember that real users interact with the thing they are building. As you build, narrate the details that production code needs.
- Make interactive elements keyboard reachable and use the right semantic element: a
buttonfor an action rather than a clickablediv. A real button gives you focus, Enter and Space handling, and the correct role for free. - Handle the empty, loading, and error states, not only the happy path.
- Manage focus when a dialog opens or results appear, so screen reader and keyboard users are not lost. Return focus to the trigger when the dialog closes.
- For a typeahead specifically, the combobox pattern wants
aria-expanded,aria-activedescendant, and arrow-key navigation. You do not need to recite every attribute, but naming the pattern shows depth. The WAI-ARIA Authoring Practices is the canonical reference if you want to study one source.
Even a sentence or two about accessibility sets you apart, because many candidates skip it entirely.
Where the same prompt is weighted differently
The same typeahead is not scored the same everywhere, and the difference is the team you are interviewing with, not a rung on a ladder. Read the posting and the interviewer's role before you decide where to spend your minutes.
| Team you are interviewing with | Where the round's weight sits | What to over-invest in |
|---|---|---|
| Product or feature team | The build round: state modelling, the states a real user hits, basic accessibility | Getting something interactive on screen, then narrating the empty, loading, and error cases |
| Design systems or platform | Component API design, generic reusable props, render behaviour | Clean prop contracts, why a component re-renders, controlled versus uncontrolled inputs |
| Full-stack split | A trimmed frontend round plus a data or API question | Not over-indexing on React trivia; being ready to move to a query or an endpoint |
Seniority shifts the same prompt along a second axis: not what you build, but how much you frame it. A junior candidate is expected to get a working feature out with minimal state and to think aloud. A senior candidate is expected to name the failure modes before they appear, reason about the tradeoff of memoising versus measuring, and say what they would push back on in the requirements. Both build the same typeahead; the senior one keeps saying why. For the question banks tied to each track, see the frontend engineer interview questions and the broader full-stack engineer interview questions.
Where frontend rounds are quietly lost
- Storing derived data in state, then fighting to keep it in sync. If a value can be computed from other state, compute it.
- Forgetting to clean up effects, which leaks listeners or lets stale responses win the race.
- Reaching for a global state library when local state or a single context would do.
- Wrapping everything in
memoanduseCallbackbefore measuring, which adds noise and can slow things down. - Going silent. Talk through your plan and your tradeoffs so the interviewer can redirect you early, while it still helps.
- Over-engineering the first pass. Build the simplest thing that works, get it on screen, then improve. A blank screen at the halfway mark is the worst outcome.
How to practise
Build five small features end to end on a timer: a typeahead, a star rating, an accordion, a paginated table, and a todo list with filtering. Then implement five utilities from scratch: debounce, throttle, an event emitter, deep clone, and a promise pool. After each attempt, review three questions: was your state minimal, was your async cancellable, and was your component accessible. Keep a short log of bugs you hit so you stop repeating them.
A realistic schedule for someone working full time is three weeks: week one on language fundamentals and utilities, week two on build features, week three on mock rounds against the clock with a friend or a recording. Recording yourself is uncomfortable but it surfaces the silent stretches faster than anything else. A few weeks of this makes the build round feel routine, which is exactly the calm you want when the clock is running.
FAQ
Do I need to know data structures and algorithms for a frontend role? Usually a lighter dose than a generalist backend loop, but not zero. Expect maps, sets, recursion over trees (the DOM is a tree), and the occasional string problem. The utility round is closer to applied DSA than to LeetCode-hard puzzles.
Can I use a UI library in the build round? Ask. Many interviewers want plain React so they can see your state and event handling. Some allow a component library but will then probe what it is doing under the hood. Either way, do not let a library hide the logic they are trying to assess.
TypeScript or JavaScript? Use whichever you are faster in unless the company specifies. If you are comfortable, TypeScript can be a quiet positive because it forces you to name your data shapes, which is exactly the modelling they want to see. Do not pick it if fighting the types will slow you down.
How much should I talk? Enough that the interviewer always knows your current plan and your next step, but not a running monologue over every keystroke. Narrate decisions and tradeoffs; stay quiet during routine typing.
What if I do not finish? Common and survivable. Finish a clear slice, then state out loud what remains and how you would build it. A well-reasoned partial solution with a credible plan often scores above a rushed, buggy complete one.
Sources
- MDN: Using microtasks in JavaScript with queueMicrotask(), on how the microtask queue drains before the next task.
- MDN: AbortController, on cancelling an in-flight fetch to kill a stale-response race.
- React: Rendering Lists, on why an array index makes an unstable key.
- React: useMemo reference, on when memoisation is worth adding and when it is noise.
- WAI-ARIA Authoring Practices, for the combobox pattern behind an accessible typeahead.