JavaScript and React interview questions

Language questions first, then React: state, hooks and the answers interviewers listen for.

Short answer

A front-end interview usually splits in two. The JavaScript half asks about types and equality, scope and closures, how this is resolved, and the order in which asynchronous code runs. The React half asks what triggers a re-render, how state updates are queued, where hooks may be called, and why lists need stable keys. Both halves reward one habit: predict the output, then explain the mechanism that produces it.

Last reviewed October 13, 2026

What front-end interview questions cover

The first screen is often a quiz: what does typeof null return, what is the difference between == and ===, what do let and const change. These are filters, and short precise answers are all they need.

The technical round is where the signal is. Expect a few predict-the-output snippets, a small component to build or fix, and a conversation about something you shipped. Teams running React ask about state, effects and rendering. Teams running Node ask about asynchronous patterns and error handling. TypeScript now appears in most postings, usually as questions about typing props and about the difference between a type and an interface.

And there is a layer that is not JavaScript at all: accessibility, forms, bundle size, caching. One or two questions often come from there, so have something to say about keyboard navigation and about what you would measure on a slow page.

The snippets on this page were run in Node, and their comments record what came back.

Types, equality and the quiz questions

Loose equality converts before comparing. The MDN reference puts it plainly: unlike the strict equality operator, the equality operator attempts to convert and compare operands that are of different types, while strict equality always considers operands of different types to be different. That single sentence answers the most common screening question, and the practical rule follows from it: use ===, and use == only in the one idiom where you deliberately want null and undefined to be treated together.

console.log(0 == "0", 0 === "0");                 // true false
console.log(null == undefined, null === undefined); // true false
console.log(typeof null, typeof NaN, NaN === NaN);  // object number false
console.log(0.1 + 0.2);                             // 0.30000000000000004

Four follow-ups come out of those lines. typeof null returns object, which is a long-standing quirk and not a sign that null is an object. NaN is of type number and is not equal to itself, so you test it with Number.isNaN. Floating point arithmetic is binary, so 0.1 plus 0.2 is not exactly 0.3 and money is better handled in integer cents. And the falsy values are worth knowing by heart: false, 0, empty string, null, undefined and NaN.

The modern part of the same topic is the pair of nullish operators. The nullish coalescing operator supplies a fallback only for null and undefined, where the logical or operator fires for every falsy value, so a count of zero or an empty string survives the first and is replaced by the second. Optional chaining then lets you read a nested value without a chain of guards.

Scope, closures and this

A closure is a function that keeps access to the variables of the scope it was created in. The interview version is a counter whose state no other code can reach:

function makeCounter() {
  let count = 0;
  return () => ++count;
}
const next = makeCounter();
console.log(next(), next(), next());   // 1 2 3

The classic trap is a loop that captures its index. A var declaration is function scoped, so all three callbacks share one binding and print the value after the loop ended. A let declaration creates a new binding per iteration:

for (var i = 0; i < 3; i++) setTimeout(() => console.log("var", i), 0);
for (let j = 0; j < 3; j++) setTimeout(() => console.log("let", j), 0);
// var 3, var 3, var 3, then let 0, let 1, let 2

Say why rather than just what: var is hoisted and function scoped, let and const are block scoped and cannot be read before the declaration is evaluated.

Then this. For an ordinary function, this depends on how the function is called, which is why a callback extracted from an object loses its receiver. Arrow functions behave differently, and MDN is explicit: arrow functions do not have their own bindings to this, arguments or super, and should not be used as methods. They take this from the enclosing scope, which is exactly what you want in a callback and exactly what you do not want for a method.

const timer = {
  label: "job",
  classic() { return function () { return this?.label; }; },
  arrow()   { return () => this.label; },
};
console.log(timer.classic()(), timer.arrow()());   // undefined job

If the interviewer pushes, the repair kit is bind, call and apply, or defining the callback as an arrow function in the first place.

The event loop, promises and async await

This is the question that separates people who have debugged asynchronous code. Synchronous code runs first, then queued microtasks such as promise callbacks, then the next task such as a setTimeout callback. MDN’s microtask guide states the mechanism: the oldest runnable task in the task queue is executed during a single iteration of the event loop, after which microtasks are executed until the microtask queue is empty.

console.log("sync 1");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("sync 2");
// sync 1, sync 2, microtask, timeout

So a timeout of zero does not mean immediately; it means after the current work and all pending microtasks. Being able to say that, and to explain that a long synchronous loop blocks everything including rendering, covers most of the topic.

The second half is sequencing your own awaits. Two independent requests awaited one after another take the sum of their durations, while Promise.all starts both and waits once:

const delay = (ms, value) => new Promise((r) => setTimeout(() => r(value), ms));

async function sequential() {
  const a = await delay(100, "a");
  const b = await delay(100, "b");
  return [a, b];                 // about 200 ms
}

async function parallel() {
  return Promise.all([delay(100, "a"), delay(100, "b")]);   // about 100 ms
}

Those timings are measured, not theoretical: run both and the first takes a little over 200 milliseconds, the second a little over 100. Finish the answer with error handling, because interviewers ask: await inside try and catch behaves like synchronous error handling, a rejected promise with no catch is an unhandled rejection, and Promise.all rejects as soon as any input rejects, while Promise.allSettled waits for all of them and reports a status for each.

Objects, copies and immutability

Spread syntax and Object.assign copy one level. The nested array in the example below is the same array in both objects, which is the source of a whole family of bugs, and in React it is the reason a state update sometimes does not appear on screen:

const original = { name: "ana", tags: ["a"] };
const copy = { ...original };
copy.name = "ben";          // only the copy changes
copy.tags.push("b");        // both see this
console.log(original.name, original.tags);   // ana [ 'a', 'b' ]

const cloned = structuredClone(original);
cloned.tags.push("c");
console.log(original.tags, cloned.tags);     // [ 'a', 'b' ] [ 'a', 'b', 'c' ]

structuredClone is the built-in deep copy in current browsers and in Node, and it is a better answer than the old JSON round trip, which silently drops functions, undefined values and dates become strings.

The array methods question belongs here too. map returns a new array of the same length, filter returns a shorter one, reduce folds the array into a single value, and forEach returns nothing. The pair that catches people is sort and reverse, which mutate the array in place, so you copy first with slice or spread when the original matters. Being able to say which methods mutate is the point of the question.

React questions: state, hooks and keys

Start with rendering, because most React answers depend on it. A component re-renders when its state changes, when its props change, or when a parent re-renders. Treat state as read-only and replace it rather than editing it, which is where the shallow-copy section above pays off.

The question that shows whether you have read the documentation is why several set calls with the same value behave as one. React batches updates and waits until all the code in the event handler has run before processing them, so the state variable stays a snapshot of that render. The documented fix is an updater function: instead of passing the next state value, pass a function that calculates the next state based on the previous one in the queue.

setCount(count + 1);
setCount(count + 1);    // one increment: count is the same snapshot twice

setCount((c) => c + 1);
setCount((c) => c + 1); // two increments: each updater sees the queued value

Next come the rules of hooks, and the docs are strict: do not call hooks inside loops, conditions, nested functions or try and catch blocks, always call them at the top level of your React function before any early returns, and call them only from components or from other hooks. The reason is that hooks are matched to their state by call order.

Effects are the other half. The useEffect reference describes the contract: React compares each dependency with its previous value, your setup function may return a cleanup function, and after every commit with changed dependencies React runs the cleanup with the old values and then the setup with the new ones, plus the cleanup once more after the component is removed. The debounced search box is the canonical example, and it is worth being able to write it from memory:

function SearchBox({ onSearch }) {
  const [query, setQuery] = useState("");

  useEffect(() => {
    if (query.length < 3) return;
    const id = setTimeout(() => onSearch(query), 300);
    return () => clearTimeout(id);
  }, [query, onSearch]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

Without the cleanup, every keystroke leaves a live timer and the handler fires once per character. With it, only the last one survives.

Keys are the last staple. A key tells React which list item is which between renders, so it must be stable and tied to the data, not to the array index, or React will reuse the wrong element when the list reorders and inputs inside the rows will hold the wrong values. Round it off with useMemo and useCallback, which exist to skip expensive recalculation and to keep a callback identity stable, and with the honest caveat that wrapping everything in them costs more than it saves. Say that you would profile first.

The practical task and role variants

The build task is usually small and deliberately open: a search box with debouncing, a list with a filter, a form with validation, or a counter with a reset. The interviewer is watching how you decide where state lives, how you name things, and whether you handle the loading and error states rather than only the happy path.

Talk while you work. Say which component owns the state and why, mention that you would lift it if a sibling needed it, and name the accessibility basics as you go: a real label for the input, a button that is a button, focus that stays visible. If data fetching is involved, say what happens while the request is in flight and what the user sees if it fails.

The variants are predictable. For a Next.js role, expect questions about rendering on the server against the client and about where data fetching belongs. For React Native, expect navigation, lists and platform differences. For a Node role, expect asynchronous error handling and how you structure an API. Pick the one in the job description and prepare it properly instead of skimming all of them.

Practice it now

Answer out loud, as if the interviewer just asked. Then check yourself against the list.

“Build a component that fetches search results as the user types, with debouncing, a loading state and an error state. Then explain what runs on every keystroke and what cleanup you needed.”

Aim for 25 minutes
  • State is declared at the top level of the component, never inside a condition or loop.
  • The effect lists every reactive value it uses in its dependency array.
  • The effect returns a cleanup function that clears the timer or aborts the request.
  • List items use a stable key from the data rather than the array index.
  • Loading and error states are rendered, not just the successful result.
  • You said out loud which component owns the state and why it lives there.

Frequently asked questions

What JavaScript questions come up in almost every interview?

The difference between == and ===, var against let and const, closures, how this is resolved and what arrow functions change, the event loop with promises against setTimeout, and shallow against deep copying. Array methods and which of them mutate are close behind.

How much React should I know for a mid-level role?

Components and props, state with the updater form, the rules of hooks, useEffect with dependencies and cleanup, lists with stable keys, controlled form inputs, and lifting state up. Context and a data-fetching library in outline are usually enough; deep internals are rarely asked.

Do I need Redux?

Only if the posting mentions it. Be ready to say when global state is worth it and when component state or context is enough, and to describe the one-way flow of actions, a reducer and a single store. Many teams now use a server-state library instead.

Will they ask me to code without an editor?

Sometimes, in a shared document with no autocomplete. Practise writing a component and a reduce call by hand, and if you forget an exact API name, say what you want it to do and keep going. Reasoning is graded far more heavily than recall.

What about TypeScript?

Expect typing component props, a union type for a status field, and the difference between a type alias and an interface. The practical answer to that last one is that interfaces merge and suit object shapes you extend, while type aliases handle unions and mapped types.

Sources

  1. MDN: Equality (==) operator
  2. MDN: Arrow function expressions
  3. MDN: Using microtasks in JavaScript with queueMicrotask()
  4. React docs: Queueing a series of state updates
  5. React docs: Rules of Hooks
  6. React docs: useEffect reference