TypeScript fundamentals
The warm-up round. Almost every TypeScript interview opens here, and a vague answer to any of these sets the tone for the rest.
01What is TypeScript, and what does it actually give you over JavaScript?Junior
TypeScript is JavaScript plus a static type system that is checked at compile time and then erased. It catches a whole class of bugs before the code runs, and it is what makes editor tooling (autocomplete, safe renames, jump to definition) trustworthy on a large codebase.
- A superset. Every valid JavaScript program is syntactically valid TypeScript, so a codebase can adopt it file by file.
- Compile-time only.
tsc(or Babel, SWC, esbuild) strips the types and emits plain JavaScript. Nothing about your types exists at runtime. - Structural. Two types are compatible when their shapes match, not when their names match.
- Inferred. You annotate the boundaries (parameters, public return types) and the compiler works out the rest.
Beyond catching mistakes, the types are documentation that cannot go stale, and they make refactoring mechanical: change a function signature and the compiler lists every call site you broke.
The trap: TypeScript does not validate data at runtime. A fetch response typed as User is a claim, not a check. If the server sends something else, the bug surfaces far from where the lie was told.
- Does TypeScript make the code faster?
- No. Types are erased, so the emitted JavaScript runs exactly as hand-written JavaScript would. The win is in correctness and maintenance, not performance.
- What does it cost?
- A build step, compile time on large projects, a learning curve for the advanced type features, and the temptation to trust types at boundaries where they were only asserted (
any,as, untyped JSON).
02What is the difference between any, unknown, never and void?Junior
any switches the type checker off for a value. unknown is the safe version: it accepts anything, but you must narrow it before using it. never is the type with no values, for code that cannot be reached. void means a function's return value is not meant to be used.
Reach for unknown wherever a value arrives from outside the type system: JSON.parse, a catch clause, postMessage, a third-party callback. It forces the check that any lets you skip.
The trap: any is contagious. One any return type silently turns every value derived from it into any as well, and the compiler stops reporting anything along that path.
- Where does
nevershow up in real code? - As the return type of functions that always throw or loop forever, as the type left over once a union has been fully narrowed (which powers exhaustiveness checks), and as the way to drop members inside conditional types.
- What is the difference between
voidandundefined? - A function typed to return
undefinedmust return exactly that. A callback typed() => voidmay return anything, and the caller promises to ignore it. That is whyitems.forEach((item) => list.push(item))type-checks even thoughpushreturns a number. - What is the type of the variable in a
catchclause? unknownunderstrict(viauseUnknownInCatchVariables), because anything can be thrown. Narrow withinstanceof Errorbefore reading.message.
03interface or type: what is the difference, and when do you use each?Junior
For object shapes they are nearly interchangeable. An interface can be extended and reopened (declaration merging); a type alias can name anything, including unions, tuples and mapped or conditional types, but cannot be reopened. Pick one convention for objects and use type whenever you need something an interface cannot express.
Interview line: I default to interface for public object shapes that others may extend, and type for unions, function signatures and anything computed. What matters most is that the team is consistent.
- When is declaration merging actually useful?
- Augmenting types you do not own: adding a property to
Window, extending a library'sThemeor session type, or adding fields to Express'sRequest. - Is there a performance difference?
- On very large codebases, yes, slightly.
interface extendsrelationships are cached by name, while big intersections are re-evaluated structurally. It only matters at scale.
04How does type inference work, and what does as const do?Mid
TypeScript infers a type from the initializer. A let or a mutable property is widened to the general type (string), while a const keeps the literal ("dark"). as const asks for the narrowest possible type everywhere: literals stay literals, objects become readonly, arrays become readonly tuples.
The classic bug it fixes is a literal that widened before it reached a function expecting a union:
The trap: as const is compile-time only. It does not freeze anything: the object can still be mutated by untyped code at runtime. Object.freeze is the runtime tool, and it is shallow.
- When should you write an annotation instead of relying on inference?
- At boundaries: function parameters always, and return types of exported functions so a change in the body cannot silently change the public contract. Locals are best left to inference.
- What is a
consttype parameter? - Since TypeScript 5.0,
function f<const T>(value: T)infersTas if the caller had writtenas const, sof(["a", "b"])givesreadonly ["a", "b"]instead ofstring[].
05What are union and intersection types?Junior
A union A | B is a value that is one or the other, so you may only use what both have in common until you narrow. An intersection A & B is a value that is both at once, so it has every member of each.
The naming feels backwards for objects until you think in sets of values: a union admits more values, so you know less about any one of them; an intersection admits fewer values, so you know more.
The trap: Intersecting object types with a conflicting property does not error. The property becomes never and the type is unusable. interface extends reports the conflict instead.
- How do you safely use a union of object types?
- Give every member a shared literal property (a discriminant such as
kindorstatus) and switch on it. That is a discriminated union, and it is the single most useful pattern in application TypeScript.
06TypeScript is structurally typed. What does that mean, and what are excess property checks?Mid
Compatibility is decided by shape: if a value has at least the required members with compatible types, it is assignable, whatever its declared name. The one exception is a fresh object literal, where extra properties are reported because they are almost always typos.
Languages like Java and C# are nominal: a Vector is a Point only if it says so. Structural typing fits JavaScript, where objects are routinely built as literals and passed around without classes.
- How do you get nominal behaviour when you need it, say to stop a
UserIdbeing passed as anOrderId? - Brand the type: intersect the primitive with a phantom property that only a constructor function can add. The brand exists only at the type level; at runtime the value is still a plain string.
- Why are the excess property checks limited to literals?
- Because a literal has no other use, an unknown key is certainly a mistake. A variable may legitimately carry more than this consumer needs, and rejecting it would break ordinary subtyping.
07How would you stop two IDs that are both strings from being mixed up?Senior
With branded (opaque) types: intersect the primitive with a unique phantom property, and only hand out the brand through a constructor or validator. Structurally identical strings then become mutually unassignable.
The same trick encodes validation in the type: type Email = Brand<string, "Email"> can only be produced by a function that actually checked the format, so any function accepting Email knows the check already happened.
08How does TypeScript handle null and undefined? What do ?., ?? and ! do?Junior
With strictNullChecks on, null and undefined are their own types and must be handled before use. ?. short-circuits to undefined when the left side is nullish, ?? supplies a fallback only for null or undefined, and the postfix ! tells the compiler to trust you, with no runtime check at all.
The trap: ! is an assertion, not a guard. Every ! is a place where the compiler stopped helping, so treat it like as: fine when you can explain why it holds, a smell when it is there to make an error go away.
- Is
name?: stringthe same asname: string | undefined? - Not quite. The optional form lets you omit the key entirely; the union form requires the key to be present, even if its value is
undefined. WithexactOptionalPropertyTypesthe optional form also refuses an explicitundefined. - What happens with
strictNullChecksoff? nullandundefinedbecome assignable to every type, sostringreally meansstring | null | undefinedand the compiler cannot warn about the most common runtime error in JavaScript.
09What is the difference between a type annotation, a type assertion (as) and satisfies?Mid
An annotation checks the value and then widens it to the annotated type. An assertion overrides the compiler without checking. satisfies checks the value against a type but keeps the value's own, more precise inferred type.
- Annotation (
const x: T = ...): use when you want the variable to have exactly typeT. satisfies: use for config objects, route tables and lookup maps, where you want validation plus precise keys and literal values.- Assertion (
as T): use when you know something the compiler cannot, for exampledocument.getElementById("email") as HTMLInputElement.
The trap: An assertion is only allowed between types that overlap, which is why you sometimes see value as unknown as T. That double assertion compiles for any two types and is the loudest escape hatch in the language.
- Why can you not use the
<T>valueassertion syntax in a.tsxfile? - It is ambiguous with JSX, so only
value as Tis allowed there. - Can you combine them?
- Yes:
{ ... } as const satisfies Configgives literal, readonly types and validation in one expression.
10What is a tuple, and how is readonly different from const?Junior
A tuple is an array type with a fixed length and a known type at each position. const stops a variable from being reassigned at runtime; readonly stops a property or array from being mutated through that reference at compile time. They solve different problems and are often used together.
Without as const, the hook above would return (boolean | (() => void))[], and every caller would have to narrow each element. This is why custom hooks that return a pair almost always end in as const.
- Is
readonlydeep? - No.
readonly items: Item[]stops you reassigningitems, not pushing to it. Usereadonly Item[]orReadonlyArray<Item>for the array, and a recursiveDeepReadonlytype if you need it all the way down. - Can a mutable array be passed where a readonly one is expected?
- Yes, a mutable array is assignable to a readonly one. The reverse is an error, which is why accepting
readonly T[]in function parameters is the more flexible signature.
11How do you type functions: optional, default and rest parameters, and overloads?Mid
Parameters are annotated individually; ? makes one optional, a default value makes it optional and infers its type, and a rest parameter is typed as an array or tuple. Overloads list several call signatures over one implementation, for when the return type depends on which arguments were passed.
- Callers only see the overload signatures. The implementation signature must be compatible with all of them but is not itself a valid call.
- Overloads are resolved top to bottom, so list the most specific one first.
- Prefer a union parameter or a generic when the return type does not change. Overloads earn their place only when different inputs produce different output types.
- Two overloads take
(a: string)and(a: string, ...rest: number[]). What must the implementation look like? - It must accept every call the overloads allow, so
(a: string, ...rest: number[])works: a rest parameter is already optional, and is simply empty for the first overload. There is no need to loosen it toany[]. - How do you type
thisinside a function? - With a fake first parameter:
function onClick(this: HTMLButtonElement, event: MouseEvent). It is erased from the output and only tells the checker whatthiswill be.
12What do public, private, protected and readonly do on a class? How is private different from #private?Junior
They control who may touch a member: public (the default) anyone, protected the class and its subclasses, private only the class. readonly allows assignment only in the declaration or constructor. TypeScript's private is enforced by the type checker and erased; JavaScript's #field is enforced by the engine at runtime.
The trap: The two bugs interviewers plant in class snippets: reading a field without this. inside a method, and assigning this.make = make in a constructor without declaring make on the class. Both are compile errors, and both are easy to read past.
- Abstract class or interface?
- An interface is a pure contract, erased at compile time, and a class can implement many. An abstract class exists at runtime and can carry state and shared implementation, but a class can extend only one. Use an interface unless you need to share code.
- What does
implementsactually do? - It only checks that the class satisfies the interface. It does not add members or change the class's type, so you still have to declare and type every property yourself.
- What is
strictPropertyInitialization? - Part of
strict: every declared property must be assigned in its declaration or in the constructor, otherwise its type would be lying about never beingundefined.
13What is the difference between object, {} and Record<string, unknown>? And how do index signatures work?Mid
object is any non-primitive. {} is anything except null and undefined, which includes numbers and strings. Record<string, unknown> is the one you usually want for 'some object with string keys': you can read any key, and each value must be narrowed before use.
An index signature says 'any key of this type maps to this value type', which is convenient and slightly dishonest:
A favourite exercise is typing a merge function. Parameters typed as object do compile, spread included, but the return type is object, on which you can access nothing. Generics keep the shape:
- What does
noUncheckedIndexedAccesschange? - Reading through an index signature or an array index yields
T | undefined, soprices.pearanditems[0]must be checked before use. It is not part ofstrict, and it is worth turning on. - When do you use a
Mapinstead of an object? - When keys are dynamic or not strings, when you need insertion order with frequent adds and deletes, or when you want
.get()to be honest about missing entries.
14Enums, unions of string literals, or an as const object: which do you use?Mid
A union of string literals is the default: it costs nothing at runtime and narrows perfectly. An as const object adds a runtime value you can iterate over while still deriving the union from it. Enums work, but they are one of the few TypeScript features that emit code, and string enums are nominal, which surprises people.
The trap: A numeric enum parameter accepts any number variable, so setDirection(someNumber) compiles even when the value is 99. String enums and literal unions do not have this hole.
- What is a
const enum? - An enum whose members are inlined at each use site, leaving no object behind. Inlining needs whole-program knowledge, which single-file transpilers (Babel, esbuild, SWC) do not have, so
isolatedModulesrestricts it and most teams avoid it. - Why do some teams ban enums outright?
- They are not erasable syntax, so they do not work with type-stripping runtimes (Node's built-in TypeScript support,
erasableSyntaxOnly), and a union oras constobject does the same job with plain JavaScript semantics.
15Types are erased at runtime. How do you make data from an API trustworthy?Mid
Treat everything that crosses a boundary as unknown and validate it once, at the edge: with a type guard for small shapes, or a schema library such as Zod or Valibot, from which the static type is derived. Inside the boundary the types can then be trusted.
Hand-written guards do not scale past a few fields. A schema gives you the validator and the type from one definition:
Interview line: Boundaries I validate: HTTP responses, form input, URL params, localStorage, postMessage, environment variables. Everything else I let the compiler handle.
- Why not just write
as User? - It moves the failure. Instead of an error at the boundary saying the payload was wrong, you get
undefined is not a functionthree components away, with a type that swears the property exists. - How do you keep the client types in sync with the server?
- Generate them from the source of truth (OpenAPI, GraphQL codegen) or share them end to end (tRPC, a shared schema package). Hand-copied interfaces drift.
Want reps on this? Drill TypeScript Types & Config →
Narrowing and unions
How the compiler follows your `if` statements. Discriminated unions and exhaustiveness checks are the part of TypeScript you use every day, so expect to write one on the spot.
01What is type narrowing, and what are the ways to narrow a type?Junior
Narrowing is the compiler refining a broad type to a more specific one by following your runtime checks. Inside an if (typeof x === "string") block, x is a string. It works with typeof, equality, truthiness, in, instanceof, Array.isArray, discriminant properties and custom type guards.
This is control flow analysis: the compiler tracks the type of each variable through branches, early returns, assignments and throw. You rarely annotate anything; you write the checks you would write in JavaScript anyway, and the types follow.
The trap: Truthiness narrowing removes null and undefined, but it also filters out 0, "" and NaN. if (!count) return is a bug when zero is a valid count. Compare against undefined or null explicitly.
- Why does
typeof value === "object"not rule outnull? - Because
typeof nullis"object", a quirk JavaScript has had since its first version. The compiler knows, and keepsnullin the narrowed type until you check for it. - What does
innarrow when the value isunknown? - After
typeof value === "object" && value !== null && "id" in value, the type becomesobject & Record<"id", unknown>, so you can readvalue.idand narrow that in turn. That is how hand-written validators are built.
02What is a user-defined type guard, and how is it different from a function that returns boolean?Mid
A type guard is a function whose return type is a type predicate, value is T. When it returns true, the compiler narrows the argument to T at the call site. A plain boolean return carries no type information, so nothing is narrowed.
An assertion function is the same idea for code that throws instead of branching:
The trap: The compiler trusts a predicate; it does not verify it. function isUser(v: unknown): v is User { return true; } compiles, and every caller now holds a lie. A guard's body deserves the same care as a type assertion.
- Do you always have to write the predicate by hand?
- Not since TypeScript 5.5: a function with no return annotation whose body is a simple narrowing expression gets an inferred predicate, so
mixed.filter((n) => n != null)isnumber[]on its own. An explicit: booleanannotation switches that inference off, andfilter(Boolean)still does not narrow. - What are the rules for the predicate?
- It must name one of the function's parameters (or
this), and the asserted type must be assignable to that parameter's declared type.
03What is a discriminated union, and how do you make sure a switch handles every case?Mid
A discriminated union is a union of object types that share one literal property, the discriminant. Checking that property narrows to a single member. For exhaustiveness, assign the value to never in the default branch: if a case is missing, the leftover type is not never and the code stops compiling.
Here is what the check catches. A triangle was added to the union and one function was not updated:
Interview line: I model state as a union of the states that can actually occur, not as one object with optional fields. { loading: boolean; data?: T; error?: Error } allows loading with data and an error at once; the union makes that state unrepresentable.
- What can be a discriminant?
- Any property whose type is a literal in every member: string and number literals,
true/false,null/undefined. Strings are the convention because they read well in aswitch. - Does narrowing survive destructuring?
- Partly. When every member has the same property names,
const { kind, payload } = actionfollowed by a check onkindnarrowspayloadtoo (since 4.6), provided both areconstbindings. If in doubt, switch onaction.kinddirectly.
04What is the return type of a function that always throws, and why does it matter?Mid
never: the function has no reachable end point. It matters because the compiler treats a call to a never-returning function as the end of that code path, so narrowing continues after it exactly as it would after a throw or return.
A detail that catches people out: the inferred return type depends on how the function is written. An arrow or function expression that only throws is inferred as never, but a function declaration is inferred as void, so annotate it.
void: the function returns, but its value should be ignored.never: the function does not return at all.neveris also what remains when a union is narrowed to nothing, which is the mechanism behind exhaustiveness checks.
- For the narrowing to work, does
failneed anything special? - Yes: an explicit
neverreturn annotation, and it must be called as a statement. The compiler only does this analysis for functions whose type is declared, not inferred.
05Why does Object.keys(obj) return string[] instead of (keyof T)[]?Mid
Because types are structural and open: a value of type T is guaranteed to have T's properties, but nothing stops it having more. Object.keys returns every key present at runtime, so promising keyof T would be unsound.
The unsoundness is easy to demonstrate, which is why the standard library refuses to hide it:
- Same question for
for...in? - Same answer: the loop variable is
string, for the same reason, and it also walks inherited enumerable properties.
06Where does narrowing break down?Senior
Narrowing is tied to a specific variable at a specific point in the control flow. It is lost when the compiler cannot prove the value is still the same: in callbacks over a variable that is reassigned later, across function calls for anything it cannot track, and through helpers that return a plain boolean.
- Reassigned captures. A closure may run later, so narrowing only survives into it when the variable is never assigned again afterwards. Copy to a
const. - Opaque checks.
if (isValid(user))narrows nothing unlessisValidis a type guard. filter(Boolean)does not removenullfrom the element type; a predicate (written or inferred) does.- Property narrowing is optimistic. After
if (obj.value !== null), a function call that mutatesobjdoes not reset the narrowing. The compiler accepts this unsoundness on purpose, because resetting on every call would make narrowing useless. - Generics. Inside a generic function,
typeofchecks narrow the value but not the type parameterTitself, so the return type often still needs an assertion or overloads.
- How do you narrow by a computed key, like
obj[key]? - Since 5.5 the compiler narrows
obj[key]when bothobjandkeyare effectively constant. The portable fix is the same as for closures: read it into aconstand narrow that.
Want reps on this? Drill Type Narrowing →
Generics and utility types
Where mid-level interviews spend most of their time. You should be able to write a constrained generic function from memory and explain every built-in utility type.
01What are generics, and what problem do they solve?Junior
A generic is a type parameter: a placeholder filled in by the caller. It lets one function, class or type work over many types while keeping the relationship between its inputs and outputs, which any throws away.
The same idea applies to types, interfaces and classes:
Interview line: A generic links types together. If a function takes a T and returns a T, the caller gets back exactly what they put in, without me knowing what it is.
- How is
Tdecided? - By inference from the arguments wherever possible. You only pass it explicitly when there is nothing to infer from (
useState<User | null>(null),new Map<string, number>()), or to override a too-narrow inference. - Do generics exist at runtime?
- No. They are erased with every other type, so you cannot write
new T()orvalue instanceof T. If you need the type at runtime, pass a value that carries it: a constructor, a schema, a type guard.
02What is a generic constraint? Write a type-safe getProperty(obj, key).Mid
T extends Something restricts what a type parameter may be, which is what lets the function body use that shape. For getProperty, constrain the key to keyof T and return the indexed access type T[K].
keyof Tis the union of T's keys: here"id" | "name" | "tags".K extends keyof TletsKbe inferred as the one literal the caller passed.T[K]looks up the type of that property, so each call returns a different, exact type.
The trap: Writing key: keyof T without the second type parameter compiles, but the return type becomes the union of every property type (string | number | string[]), because the compiler no longer knows which key was passed.
- Does
extendshere mean inheritance? - No. In a constraint it means 'is assignable to'.
T extends string | numberacceptsstring,number, and any literal or union of them. - How would you type a
pluck(items, key)that returns an array of that property? function pluck<T, K extends keyof T>(items: T[], key: K): T[K][], returningitems.map((item) => item[key]).
03When is a generic the wrong tool?Senior
When the type parameter appears only once. A generic exists to relate two or more positions; if it relates nothing, it is unknown with extra syntax. And a type parameter that appears only in the return type is a type assertion in disguise.
- Rule of thumb: every type parameter should appear at least twice in the signature.
- Push constraints down. Prefer
<T>(items: T[])over<A extends unknown[]>(items: A): the simpler the parameter, the better the inference. - Defaults (
<T = unknown>) keep a generic type convenient when most callers do not care.
- How do you stop one argument from widening the inferred type?
- Wrap the other positions in
NoInfer<T>(5.4). IncreateMachine<S extends string>(states: S[], initial: NoInfer<S>),Sis inferred only fromstates, so a misspeltinitialis an error instead of a new member of the union.
04What do keyof, typeof and indexed access types do? How do you derive a type from a value?Mid
typeof value gives the static type of a value. keyof T gives the union of T's keys. T[K] looks up the type of a property. Together they let you write the data once and compute the types from it, so the two cannot drift apart.
[number] indexes an array or tuple type by 'any numeric index', which yields its element type. It is the standard way to turn a const array into a union.
- How is this
typeofdifferent from JavaScript's? - In an expression,
typeof xis the runtime operator and returns a string. In a type position it is a type query and returns the full static type. Same keyword, two languages. - What is
keyofof a union? - Only the keys common to every member, because those are the only ones that are safe to access.
keyof (A | B)iskeyof A & keyof B.
05Which built-in utility types do you use, and what does each one do?Junior
The everyday ones reshape object types (Partial, Required, Readonly, Pick, Omit, Record), filter unions (Exclude, Extract, NonNullable) and read function types (Parameters, ReturnType, Awaited). They are all ordinary mapped and conditional types that ship with the compiler.
The trap: Omit does not check its keys, so Omit<User, "pasword"> compiles and removes nothing. It also does not distribute over unions: applied to a discriminated union it flattens the members into one object type and the discrimination is lost.
- How do you fix
Omitfor a union? - Make it distribute:
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never. The conditional type runs once per member and re-unions the results. - How would you make just one property optional?
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>. Composing the built-ins usually beats writing a new mapped type.
06Implement Partial, Pick, Omit, Exclude and ReturnType yourself.Senior
The object utilities are mapped types ([K in keyof T]) with modifiers; the union utilities are distributive conditional types; the function utilities use infer to pull a type out of a pattern.
If you are asked how you would know these are right, the answer is a type-level test. Equal compares two types exactly, and Expect fails to compile when it is given false:
- Why does the mapped-type version keep
readonlyand?from the original? - Mapping over
keyof Tis a homomorphic mapped type: the compiler knows the keys come fromTand copies each property's modifiers across. Mapping over an unrelated union of strings does not. - Why is
MyOmitconstrained toPropertyKeyrather thankeyof T? - That matches the built-in. It lets you omit keys that might not exist, which is convenient for generic code and is also exactly why typos are not caught.
Want reps on this? Drill TypeScript Generics →
Advanced types
Mapped, conditional and template literal types, plus variance. Senior interviews use these to find out whether you understand the type system or only use it.
01What is a mapped type? What does the as clause do?Mid
A mapped type builds a new object type by iterating over a union of keys: { [K in Keys]: ... }. Modifiers (readonly, ?, and their removals -readonly, -?) can be added per key, and an as clause renames or filters the keys as it goes.
Key remapping (4.1) puts an as clause after the key. Mapping a key to never drops it, which turns a mapped type into a filter:
string & Kis there becausekeyof Tmay includenumberandsymbolkeys, andCapitalizeonly accepts strings.Capitalize,Uncapitalize,UppercaseandLowercaseare intrinsic string-manipulation types.
- What happens when you map over an array or tuple type?
- A homomorphic mapped type applied to an array or tuple maps the element types and returns an array or tuple, not an object with numeric keys. That is why
Readonly<string[]>isreadonly string[]. - Where do you actually use this outside puzzles?
- Deriving form state from a model (
{ [K in keyof T]: FieldState<T[K]> }), typing event maps, generating handler names for a component API, and stripping or addingreadonlyat a module boundary.
02What are conditional types, and how does infer work?Senior
A conditional type is a type-level ternary: T extends U ? X : Y. Inside the extends clause, infer R declares a type variable that the compiler fills in by pattern-matching, so you can pull a piece out of a larger type.
When the checked type is a bare type parameter and you pass it a union, the conditional type is applied to each member separately and the results are unioned. This is distribution, and it is how Exclude works:
The trap: Wrapping both sides in a tuple, [T] extends [U], switches distribution off. Forgetting this is the usual reason a conditional type returns a union you did not expect, or never when you pass it never.
- Where may
inferappear? - Only inside the
extendsclause of a conditional type, and the variable is only in scope in the true branch. Since 4.7 it can carry a constraint:infer U extends string. - Name two built-ins that use it.
ReturnType<F>isF extends (...args: any) => infer R ? R : any, andAwaited<T>recursively infers the value type of a thenable.
03What are template literal types?Senior
String literal types built with template syntax. Interpolating a union produces every combination, and combined with infer they can parse a string type, which is how routers and CSS-in-TS libraries type paths and class names.
With infer, a template literal type becomes a parser. This one extracts the parameter names from a route:
The trap: Unions multiply. Two 50-member unions interpolated together produce 2,500 literal types, and the compiler gives up above 100,000. Keep the inputs small.
04Write a DeepReadonly<T>. Why does it need a conditional?Senior
A type may refer to itself, which lets it walk a nested structure. DeepReadonly maps every property to readonly and recurses into the ones that are objects. The conditional is the base case: without it the recursion would try to map over primitives and functions.
The same shape gives you DeepPartial, and a recursive alias is the natural way to describe JSON:
- Arrays work because a mapped type over an array type maps its elements:
string[]becomesreadonly string[]. Date,MapandSetare objects too, so this simple version maps over their members. A production version special-cases them (ReadonlyMap,ReadonlySet).- Recursion depth is limited; very deep or self-referential data can hit 'Type instantiation is excessively deep'.
05What are covariance and contravariance? What do the in and out annotations do?Senior
Variance describes how subtyping of a type argument carries over to the generic type. Output positions are covariant (a producer of Dog is a producer of Animal); input positions are contravariant (a consumer of Animal is a consumer of Dog). out T and in T declare that intent explicitly so the compiler checks it and skips the structural comparison.
TypeScript is deliberately unsound in one familiar place: mutable arrays are treated as covariant.
The trap: Parameter contravariance is only enforced under strictFunctionTypes, and only for function-typed properties. Methods written with method syntax (accept(value: T): void) are still compared bivariantly. Declaring callbacks as properties (onChange: (value: T) => void) gets you the strict check.
- Why would you add
inoroutif the compiler can already work variance out? - Documentation of intent, faster type-checking of deeply recursive generics, and an error at the declaration if someone later uses
Tin the wrong position. They do not change behaviour; a wrong annotation is rejected. - Why can a React
onChangethat accepts aChangeEvent<HTMLInputElement>not be passed whereChangeEvent<HTMLElement>handlers are expected? - Contravariance. That handler may read
event.target.value, so it cannot safely be called with an event from an arbitrary element. A handler for the wider type can be used for the narrower one, never the reverse.
06Type puzzles: write First, Last, TupleToUnion, Length and If.Senior
Tuple puzzles come down to three moves: index with [number] or ["length"], pattern-match with a variadic tuple and infer, and branch with a conditional type.
Interview line: I talk through the base case first, then the recursive or pattern-matching case, and I write one or two Expect<Equal<...>> lines before the implementation so the interviewer can see me checking the edge cases: the empty tuple, never, a union.
- Why is
First<T> = T[0]not quite right? - For the empty tuple,
[][0]isundefinedrather thannever, which usually is not what the puzzle wants. Theinferversion handles the empty case explicitly. - Should this kind of type appear in application code?
- Rarely. It belongs in libraries and shared utilities, where the complexity is paid once and hidden. In feature code, a type that needs a comment to be understood is usually a sign that the data model could be simpler.
Want reps on this? Drill TypeScript Generics →
Config, modules and declarations
The questions that separate people who have written TypeScript from people who have set it up: compiler flags, declaration files, the build pipeline, and migrating a real codebase.
01What does strict turn on, and which other compiler options do you care about?Mid
strict is a bundle of checks, the most important being strictNullChecks and noImplicitAny. Without it TypeScript is closer to a linter than a type system. Beyond strict, noUncheckedIndexedAccess is the flag most worth adding, and isolatedModules is required by every modern bundler.
noUncheckedIndexedAccess:array[i]andrecord[key]becomeT | undefined.exactOptionalPropertyTypes:name?: stringno longer accepts an explicitundefined.noEmit: the bundler emits JavaScript;tsconly type-checks.skipLibCheck: do not re-check.d.tsfiles innode_modules. Faster, and avoids errors you cannot fix.target/lib: which syntax to emit, and which built-in APIs the types assume exist.
- You inherit a project with
strict: false. What do you do? - Turn the flags on one at a time,
noImplicitAnyfirst andstrictNullCheckssecond, fixing or explicitly suppressing errors per directory, so the change lands as several reviewable pull requests and not as one that touches every file. - What is the difference between
targetandlib? targetcontrols the syntax of the emitted JavaScript.libcontrols which runtime APIs the type checker believes are available. Neither adds polyfills.
02How does TypeScript get from source to something a browser runs?Mid
Two separate jobs: type-checking and emitting JavaScript. tsc can do both, but modern toolchains split them. A fast transpiler (esbuild, SWC, Babel) strips the types file by file, with no type information, and tsc --noEmit runs alongside to do the actual checking.
- Transpile: the bundler removes type syntax and compiles JSX. Each file is handled in isolation, which is why it is fast and why it cannot catch type errors.
- Type-check:
tsc --noEmitin the editor, in a pre-commit hook and in CI. This is the step that fails the build on a type error. - Bundle: modules are resolved, tree-shaken, split and minified.
The consequence worth saying out loud: your dev server will happily run code that does not type-check. The types are enforced by the editor and by CI, not by the runtime.
isolatedModulesmakes the compiler reject code a single-file transpiler cannot handle correctly.- Libraries additionally emit
.d.tsfiles (declaration: true) so consumers get types without the source. - Node can now run
.tsfiles directly by stripping types. That only works for erasable syntax, so enums, namespaces and constructor parameter properties are out;erasableSyntaxOnlyenforces the subset.
- Why does Babel or esbuild not just type-check too?
- Type-checking needs the whole program: to check one file you must resolve the types of everything it imports. Stripping syntax needs only the file itself, which is what makes it parallel and fast.
- How do you speed up type-checking on a large repo?
- Incremental builds (
incremental,tsBuildInfoFile), project references so packages are checked independently,skipLibCheck, and avoiding pathological types (huge unions, deep recursion, enormous intersections). The native port of the compiler (TypeScript 7) targets an order-of-magnitude speed-up on top of that.
03What are import type and isolatedModules for?Mid
import type marks an import as types only, so it is guaranteed to be erased. That matters because a single-file transpiler cannot tell whether an imported name is a type or a value. isolatedModules makes the compiler flag anything that would be ambiguous to such a tool.
- Without the
typekeyword, a transpiler looking at one file cannot know whetherUseris a type (drop the import) or a value (keep it, and its side effects). verbatimModuleSyntaxmakes the rule mechanical: anything markedtypeis removed, everything else is emitted exactly as written.- Type-only imports also cannot create circular-dependency problems at runtime, because they do not exist at runtime.
The trap: Under isolatedModules you cannot re-export a type with a plain export { User }, and const enum values from other files cannot be relied on, because both need knowledge of another file to compile correctly.
- Does importing a type add to the bundle?
- No. Types are erased, so a type-only import contributes zero bytes, even if the module it comes from is large.
04What is a .d.ts file, and what does declare do?Mid
A declaration file contains types with no implementation: it describes the shape of JavaScript that exists somewhere else. declare is the keyword that says 'this exists at runtime, emit nothing for it'. Libraries ship .d.ts files (or you install them from @types/*), and you write your own for globals and non-code imports.
declare constis an ambient declaration. A plainconstin a.d.tswould be an error, because declaration files cannot contain values.- Being ambient, it can be included any number of times without a duplicate-definition error.
export constwould make the file a module and the constant module-scoped, not global.
- A library has no types and no
@typespackage. What are your options? - Write a small
declare module "name"with just the functions you call; that is better than the shorthanddeclare module "name";, which types every import asany. - How do you add a typed global from inside a module file?
- Wrap it in
declare global { ... }. A file with an import or export is a module, so top-level declarations in it are local.
05What is declaration merging, and what is module augmentation?Senior
When two declarations share a name, the compiler merges them into one. Interfaces merge member by member, and namespaces merge with classes, functions and enums. Module augmentation uses this to add to types you do not own: a global like Window, or a library's interface.
- Interfaces with the same name merge. Non-function members must agree on their type; methods become overloads, later declarations first.
- Type aliases never merge. A second
type Xis a duplicate identifier error. - Namespaces merge with a class, function or enum of the same name, which is how static members and nested types are added in older declaration files.
Interview line: This is the real reason library authors publish interfaces instead of type aliases for anything users may need to extend: themes, session objects, route maps, environment variables.
- How do you type
process.envorimport.meta.env? - Augment the interface the runtime declares:
namespace NodeJS { interface ProcessEnv { API_URL: string } }for Node, orinterface ImportMetaEnv { readonly VITE_API_URL: string }for Vite. - What goes wrong with augmentation?
- It is global and invisible at the use site. Two packages augmenting the same interface with conflicting types produce errors that are hard to trace, and an augmentation in a file that is not part of the program silently does nothing.
06How do modules work in TypeScript? What about namespaces?Junior
TypeScript uses standard ES modules: a file with a top-level import or export is a module with its own scope, and anything else is a global script. Namespaces are the pre-ES-module way of grouping code and today survive mainly inside declaration files.
- Named exports are generally preferred over default exports: they rename consistently, autocomplete better and survive refactors.
moduleResolutiondecides how an import specifier becomes a file. Usebundlerfor an app built by Vite, webpack or Next, andnodenextfor code Node runs directly.- Path aliases (
pathsintsconfig) only affect type-checking. The bundler or runtime needs the same mapping configured separately. - Barrel files (
index.tsthat re-exports) tidy up imports but can defeat tree-shaking and slow down tooling when they grow.
- What does
esModuleInteropdo? - It lets you write
import express from "express"for a CommonJS module that usesmodule.exports = ..., by emitting a small helper that wraps the module as a default export. - When is a namespace still reasonable?
- In
.d.tsfiles describing a global library, and for declaration merging. For application code, an ES module does everything a namespace does and bundlers understand it.
07What are decorators, and how do they work in TypeScript?Mid
A decorator is a function applied with @name to a class or class member; it receives the thing it decorates and may wrap or replace it. They are used for cross-cutting behaviour such as logging, validation and dependency injection. TypeScript 5 implements the standard decorators; the older experimentalDecorators flavour is what Angular and NestJS were built on.
The trap: The two are not compatible. A decorator written for one signature fails under the other, so check which mode a codebase uses before answering 'how do you write a decorator'.
- Where do decorators show up in React?
- Mostly historically: MobX's
@observableand@observer, and Redux's@connecton class components. Hooks replaced almost all of it, so in a React codebase today they are rare. - What can be decorated?
- Classes, methods, accessors, fields and (with the
accessorkeyword) auto-accessors. Not standalone functions, which is why function composition or higher-order functions do that job in React.
08How do you type asynchronous code? What is the type of a caught error?Junior
An async function always returns Promise<T>, where T is the type of what you return. await unwraps it. A promise's rejection has no type, so the variable in catch is unknown and must be narrowed before you read from it.
When failure is an expected outcome and not an exception, put it in the return type, where the compiler can make callers handle it:
The trap: Awaiting inside a loop runs the requests one after another. If they are independent, start them all and await Promise.all(...). And a promise you neither await nor .catch fails silently; the no-floating-promises lint rule exists for that.
Promise.allvsallSettledvsracevsany?allrejects as soon as one input rejects.allSettledwaits for everything and reports each outcome.racesettles with the first to settle, either way.anyresolves with the first to fulfil and rejects only if all reject.- Why can you not write
Promise<User, HttpError>? - Anything can be thrown in JavaScript, including from code you call, so a rejection type could never be guaranteed. The language reflects that by typing it
unknown.
09How would you migrate a large JavaScript codebase to TypeScript?Senior
Incrementally, with the app shippable at every step. Let TypeScript and JavaScript coexist (allowJs), convert from the leaves of the dependency graph inward, start with loose settings, and tighten the compiler flags one at a time once most files are converted.
- Set up side by side. Add a
tsconfigwithallowJs: trueandstrict: false, wiretsc --noEmitinto CI, and install the@types/*packages for your dependencies. - Type the foundations first. Shared utilities, API clients and domain models have no internal dependencies and are imported everywhere, so typing them improves inference across the app immediately.
- Convert leaf to root. Rename
.jsto.tsin small pull requests. A file's imports should already be typed when you reach it. - Allow escape hatches, but track them. Use
// @ts-expect-errorwith a reason rather thanany, and lint against new explicitany. - Ratchet the strictness. Enable
noImplicitAny, thenstrictNullChecks, then fullstrict, each as its own piece of work. - Validate the boundaries. Once the inside is typed, add runtime validation where data enters, so the types describe reality.
checkJswith JSDoc annotations gives type-checking in files you have not renamed yet.@ts-expect-erroris better than@ts-ignore: it fails when the error it suppresses goes away, so suppressions do not outlive their reason.- New code is written in TypeScript from day one, so the untyped surface only shrinks.
Interview line: The goal is not zero any on day one. It is that every week there is less untyped code than the week before, and nothing new is added to it.
- How do you measure progress?
- Percentage of files converted, count of
anyand@ts-expect-error, and which strict flags are on. A type-coverage tool in CI stops the number going backwards. - What is the most common mistake?
- Turning on
stricteverywhere at once and then silencing thousands of errors withany. You end up with TypeScript syntax and none of the guarantees.
Want reps on this? Drill TypeScript Types & Config →
TypeScript with React
Where the two halves of the interview meet. Most React roles assume TypeScript, and these are the typing questions that come up while you are live-coding a component.
01How do you type a component's props, including children and default values?Junior
Declare an interface or type for the props and annotate the destructured parameter. Optional props get a ?, defaults go in the destructuring, and children is typed as ReactNode.
- Use a union of literals, not
string, for anything with a fixed set of values. It is checked and it autocompletes. defaultPropsis no longer supported on function components in React 19; default parameter values replace it.PropTypeswas the runtime predecessor of all this. It is removed in React 19, and TypeScript catches the same mistakes at compile time.
- Should you use
React.FC? - It works, and since the React 18 types it no longer adds an implicit
childrenprop. But it adds nothing over annotating the props parameter, and it cannot express generic components, so most codebases skip it. - Should you annotate the return type?
- Usually not; let it infer. If you do,
ReactNodeis the right annotation for a component.
02What is the difference between ReactNode, ReactElement and JSX.Element?Mid
ReactNode is anything React can render: elements, strings, numbers, null, undefined, booleans, arrays of those. ReactElement is specifically the object a JSX expression produces. JSX.Element is a ReactElement with the generics set to any: the type of a JSX expression.
The trap: Typing children as JSX.Element rejects strings, numbers, null, conditionals like {open && <Menu />}, and multiple children. It is almost always a mistake for ReactNode.
- How do you type
childrenas a function (a render prop)? - As a function type:
children: (state: { open: boolean }) => ReactNode. - What is the type of a component itself?
ComponentType<Props>for 'any component accepting these props', orElementTypefor 'anything valid as a JSX tag', which includes strings like"div".
03How do you type useState, useRef and useReducer?Junior
useState infers from its initial value, so you only pass a type argument when that value does not tell the whole story (null, an empty array, a union). useRef takes the element type and starts at null for DOM refs. useReducer is typed by typing the reducer, with the action as a discriminated union.
Because the reducer's return type is annotated as State and every case returns, a new action type that is not handled makes the function fall off the end, which the compiler reports. That is the exhaustiveness check for free.
- What type do you give a state setter passed as a prop?
Dispatch<SetStateAction<T>>, although it is usually better to pass a narrower callback such asonChange: (value: T) => voidso the child does not depend on the parent usinguseState.- Why does
useRef()with no argument fail in React 19's types? - The initial value is now required. Pass
nullfor a DOM ref orundefinedfor a mutable value. The separateMutableRefObjecttype is deprecated; every ref'scurrentis now assignable.
04How do you type event handlers?Junior
An inline handler needs no annotation, since the type flows from the JSX attribute. A handler declared separately takes the matching React event type, parameterised by the element: React.ChangeEvent<HTMLInputElement>, React.MouseEvent<HTMLButtonElement>, React.SubmitEvent<HTMLFormElement>.
event.currentTargetis the element the handler is attached to, and is precisely typed.event.targetis where the event started, which may be a child, so it is only typed asEventTarget(change events are the exception).- For a handler passed as a prop, type the prop with the handler alias:
onClick?: React.MouseEventHandler<HTMLButtonElement>. - Not sure which type? Write the handler inline, hover over the parameter, and copy what the editor shows.
The trap: Older code and tutorials type submit handlers as React.FormEvent. Current React types deprecate it in favour of SubmitEvent, ChangeEvent and InputEvent, which describe events that actually exist.
05How do you build a component that accepts every prop of a native element, plus your own?Mid
Intersect your props with ComponentProps<"button"> (or whichever element), then spread the rest onto the element. The wrapper gets type, disabled, aria-*, event handlers and, in React 19, ref, all typed, without listing any of them.
ref is an ordinary prop on function components since React 19, so ComponentProps<"button"> already includes it. Before 19, the same component needed forwardRef:
ComponentProps<typeof Other>borrows the props of another component, which keeps wrappers in sync with what they wrap.- Use
Omit<ComponentProps<"input">, "onChange">when you want to replace a native prop with your own signature.
- How do you expose an imperative API instead of the DOM node?
- Type the ref as
Ref<Handle>, whereHandleis an interface such as{ focus(): void; reset(): void }, and return an object of that shape fromuseImperativeHandle(ref, () => ({ ... })). - Why separate
classNamefrom...rest? - So the wrapper's own classes and the caller's are merged. Spreading
restafter a hard-codedclassNamewould silently let the caller overwrite yours.
06How do you write a generic component?Mid
Give the component function a type parameter and use it in the props, so the item type flows from one prop to the others. The caller never writes the type; it is inferred from the data they pass.
The same pattern types any 'value in, value out' control, such as a select whose options are objects:
- How do you write a generic arrow-function component in a
.tsxfile? const List = <T,>(props: ListProps<T>) => .... The trailing comma stops the parser reading<T>as a JSX tag. A function declaration avoids the issue.- Can a generic component be wrapped in
memoorforwardRef? - Not without losing the type parameter, because those helpers are typed over a fixed props type. The usual fixes are a cast back to the generic signature or, in React 19, dropping
forwardRefand takingrefas a prop.
07A component has props that only make sense in certain combinations. How do you type that?Senior
As a discriminated union of prop shapes, not as one interface full of optionals. Each variant lists exactly the props it needs, the compiler rejects invalid combinations at the call site, and inside the component a check on the discriminant narrows the props.
When there is no natural discriminant, never expresses 'one or the other, not both':
Interview line: If I find myself writing a comment like 'only pass onRetry when status is error', that is a type I have not written yet.
- Why keep
propswhole instead of destructuring in the parameter list? - Narrowing follows the
propsobject. Once the fields are pulled apart, a property that exists on only one variant cannot be destructured at all, and the relationship between the discriminant and the rest is harder for the compiler to follow.
08How would you type a polymorphic component with an as prop?Senior
Make the component generic over an ElementType, and compute its props from that type with ComponentProps<E>. Then as="a" allows href and as="button" does not.
- The local
Tagis annotated as plainElementTypebecause the compiler cannot prove that the remaining props match an unresolved genericE. The public signature is what stays precise. - Polymorphic types are expensive to check and produce long error messages, so keep them to a few primitives (
Box,Text,Button).
The trap: This is a place to show judgement, not just skill. Many design systems now prefer composition (<Button asChild><a href="/docs" /></Button>, or a render prop) because it gives the same flexibility with far simpler types.
09How do you type a context so consumers do not have to handle undefined everywhere?Mid
Create the context with null as the default, and expose it only through a custom hook that throws if the provider is missing. The hook's return type is then the non-null value, and a forgotten provider fails loudly instead of rendering with a fake default.
<AuthContext value={...}>is the React 19 form. Earlier versions write<AuthContext.Provider value={...}>.- The
useMemomatters for behaviour, not types: a new object every render would re-render every consumer. - Avoid
createContext({} as AuthContextValue). It type-checks and then fails at runtime with an unhelpful 'undefined is not a function' when the provider is missing.
10How do you type a custom hook's return value?Mid
Let it infer, with one exception: when returning a pair, add as const so the result is a tuple and not an array of a union. Use a generic when the hook stores or returns the caller's data.
- Tuple for one or two values the caller will want to rename (
const [open, toggleOpen] = ...). - Object for three or more, so call sites are self-describing and adding a field is not a breaking change.
- The
JSON.parse(...) as Tabove is an unchecked boundary. For anything that matters, pass a validator or schema into the hook.
- What does the return type look like without
as const? (boolean | (() => void))[]. Every element could be either, so callingtoggle()would not compile without a check.
11How do you type a higher-order component?Senior
Make it generic over the wrapped component's props, constrained to include the props the HOC injects. The returned component takes the original props minus the injected ones, via Omit.
- The
props as Passertion is the known rough edge: the compiler cannot prove thatOmit<P, K>plusKrebuildsPfor a genericP. - Setting
displayNamekeeps the component tree readable in React DevTools.
Interview line: I can write this, and I would then say that a custom hook, const user = useCurrentUser(), does the same job with no wrapper component, no assertion and no prop-name collisions. That is why HOCs are rare in new code.
Want reps on this? Drill TypeScript Types & Config →
React fundamentals
Asked in nearly every React interview at every level. Juniors are expected to know the definitions; seniors are expected to explain why each one is the way it is.
01What is React, and what are its main features?Junior
React is a JavaScript library for building user interfaces out of components. You describe what the UI should look like for a given state, and React works out which DOM updates are needed when that state changes.
- Declarative. You write the UI as a function of state. You do not write the steps to get from the old screen to the new one.
- Component-based. The UI is a tree of small, reusable functions, each owning its markup, logic and state.
- One-way data flow. Data moves down through props; changes are requested upward through callbacks.
- A virtual DOM with reconciliation. React diffs the new description against the previous one and touches the real DOM as little as it can.
- Hooks. State, effects and shared logic in plain functions.
- Renderer-agnostic. The same model drives the browser (React DOM), native apps (React Native) and server rendering.
React is a library, not a framework: it renders. Routing, data fetching, bundling and server rendering come from the ecosystem, which is why new projects usually start on a framework such as Next.js or React Router, or on Vite for a client-only app. Create React App is deprecated.
- What are React's limitations?
- It is only the view layer, so every project assembles (or picks a framework for) the rest. The render model takes time to internalise: stale closures, effect dependencies and unnecessary re-renders are learned the hard way. And a client-only React app ships a lot of JavaScript before anything appears, unless you add server rendering.
- How is it different from Angular or Vue?
- Angular is a full framework with its own templates, dependency injection, router and forms, and it uses two-way binding in forms. Vue sits in between, with templates and a reactivity system that tracks dependencies automatically. React is the smallest of the three in scope, uses JSX, and re-runs component functions instead of tracking fine-grained dependencies.
02What is JSX, and how does a browser run it?Junior
JSX is a syntax extension that lets you write markup inside JavaScript. Browsers cannot run it. A compiler (Babel, SWC, esbuild, TypeScript) turns each tag into a function call that returns a plain object describing the element.
- A component returns one root. Use a fragment (
<>...</>) to return siblings. - Curly braces embed any JavaScript expression, not statements: no
iforforinside JSX. - Attributes are camelCase and follow the DOM property names:
className,htmlFor,onClick,tabIndex. styletakes an object:style={{ marginTop: 8 }}.- Lowercase tags are DOM elements; capitalised tags are components.
<card />would look for an HTML element namedcard.
- Why
classNameand notclass? - JSX attributes become keys of a JavaScript object and map to DOM properties, and the DOM property is
className.classis also a reserved word in JavaScript. - Why did older code need
import React from "react"in every file? - Before React 17 the compiler emitted
React.createElement(...), soReacthad to be in scope. The automatic runtime importsjsxfromreact/jsx-runtimeitself. - How does JSX protect against XSS?
- Everything embedded with
{}is rendered as text: React escapes it before it reaches the DOM, so a string containing<script>is displayed, not executed. The escape hatch isdangerouslySetInnerHTML, which is exactly as safe as the HTML you give it. - Can you use React without JSX?
- Yes, by calling
createElement(type, props, ...children)directly. JSX is only a more readable way to write those calls.
03What is the difference between a React element and a React component?Junior
A component is a function that accepts props and returns a description of UI. An element is that description: a small immutable object saying 'render this type with these props'. Button is a component; <Button /> is an element.
- Element: cheap, immutable, recreated every render. It is the output of JSX.
- Component: the function (or class) React calls to get elements.
- Instance / fiber: React's internal record for a mounted component. This is where state lives.
- Node (
ReactNode): anything renderable, so elements plus strings, numbers,null, arrays.
The trap: Two mistakes follow from blurring these. Calling a component as a function (Welcome({ name })) makes its hooks belong to the caller. Defining a component inside another component creates a brand-new component type on every render, so React unmounts and remounts it each time and its state is lost.
04What is the virtual DOM? How is it different from the real DOM and the shadow DOM?Junior
The virtual DOM is React's in-memory description of the UI: a tree of plain objects. On each update React builds a new tree, compares it with the previous one, and applies only the differences to the real DOM. The shadow DOM is unrelated: it is a browser feature for encapsulating a component's markup and styles.
- Render. State changes, and React calls the affected components to produce a new tree of elements.
- Reconcile. It diffs the new tree against the previous one to find what changed.
- Commit. It applies the minimal set of DOM operations, then runs effects.
- What it buys you: you write the UI declaratively, and React batches and minimises DOM writes for you. It also makes other render targets possible, such as native views or server-side HTML.
- What it costs: building and diffing trees takes CPU and memory. It is not automatically faster than careful hand-written DOM code; it is fast enough while being far easier to reason about.
Interview line: The virtual DOM is not what makes React fast. It is what makes a declarative model affordable. The win is that I never write the code that moves the UI from one state to another.
- Does a re-render mean the DOM was updated?
- No. Rendering means React called your component and compared the result. If the output is identical, the commit phase changes nothing in the DOM.
- Do all frameworks use a virtual DOM?
- No. Svelte compiles components into direct DOM updates, and Solid uses fine-grained signals. They trade React's 're-run the function' model for dependency tracking.
05What is the difference between props and state?Junior
Props are inputs passed in by the parent, read-only from the component's point of view. State is data the component owns and can change over time. A change to either causes a re-render.
A useful test when deciding whether something should be state: does it change over time, is it not passed in from a parent, and can it not be computed from other props or state? Only if all three are true does it belong in useState.
The trap: Copying a prop into state (useState(props.value)) only reads the prop on the first render. When the parent later passes a new value, the state does not follow. Either use the prop directly, name it initialValue to make the one-time read explicit, or reset the component with a key.
- How does a child change something the parent owns?
- It cannot change the prop. The parent passes down a callback (
onChange), the child calls it, the parent updates its own state, and new props flow back down.
06Why must you not mutate state directly? How do you update objects and arrays?Junior
React decides whether anything changed by comparing references. If you mutate an object or array in place, the reference is the same, so React can skip the re-render, and memoised children and effect dependencies see no change. Always produce a new value.
Nested data has to be copied at every level along the path you are changing:
- What else relies on immutability besides re-rendering?
React.memo,useMemoanduseEffectdependency checks all compare by reference. Each render is also meant to be a snapshot: mutating state makes past renders, closures and concurrent rendering see data that changed underneath them.- Deep updates get ugly. What then?
- Flatten the state shape so updates are shallow, or use Immer (
useImmer, and built into Redux Toolkit), which lets you write mutating code against a draft and produces the immutable copy for you.
07Explain one-way data flow and 'lifting state up'.Junior
Data moves in one direction: down the tree through props. A child that needs to change something calls a function its parent gave it. When two components need the same data, you move that state up to their closest common ancestor and pass it down to both.
- There is a single source of truth for each piece of state, so two views of it cannot disagree.
- To find out why something changed, you follow the props up to the component that owns the state. There is nowhere else to look.
- The cost is plumbing: lifting state far up leads to prop drilling, which is the point at which composition, context or a store earn their place.
- Does React have two-way binding?
- Not built in. A controlled input is the same effect built from two one-way pieces:
valueflows down andonChangeflows up. - How high should state be lifted?
- As high as it needs to be and no higher. State kept close to where it is used re-renders less of the tree and is easier to delete.
08What are keys for? What goes wrong when you use the array index as a key?Junior
A key gives each item in a list a stable identity between renders, so React can tell which item moved, which was added and which was removed. With index keys the identity is the position, not the item: reorder or delete, and React keeps the DOM nodes and component state where they were and just swaps the contents, so state ends up attached to the wrong row.
Now imagine key={index}, the user ticks the first task, and then that task is deleted. React sees keys 0..n-2 still present and key n-1 gone. It keeps the first <li> (with its ticked checkbox), updates its label to the second task's text, and removes the last <li>. The tick has moved to a task the user never touched.
- Keys only need to be unique among siblings, not globally.
keyis not a prop: the child cannot read it. Pass the ID separately if it needs it.- For items created on the client, generate the ID once when the item is created and store it in the data.
- Can you use
keyoutside a list? - Yes, and it is the idiomatic way to reset a component.
<ProfileForm key={userId} />gives React a different identity when the user changes, so it unmounts the old form and mounts a fresh one with fresh state, with no effect needed to clear the fields. - Is an index key a performance problem or a correctness problem?
- Both, but correctness is the one that matters. Performance: inserting at the top re-renders every row. Correctness: uncontrolled inputs, focus, animations and component state stay with the position instead of the item.
09What is the difference between controlled and uncontrolled components?Junior
A controlled input gets its value from React state and reports changes through onChange, so state is the single source of truth. An uncontrolled input keeps its own value in the DOM, and you read it when you need it, through a ref or from the form on submit.
The trap: value={user?.name} starts as undefined, which React treats as uncontrolled, and then becomes a string: 'A component is changing an uncontrolled input to be controlled'. Give it a defined value from the start: value={user?.name ?? ""}. And a value with no onChange renders a read-only field.
- Which should you use for a large form?
- Controlled inputs re-render on every keystroke, which gets noticeable with many fields. Libraries like React Hook Form keep inputs uncontrolled and subscribe only where needed. React 19's form actions also lean uncontrolled: the action receives a
FormData. - Why is
<input type="file">always uncontrolled? - Its value is a file chosen by the user, which script cannot set for security reasons. You read
filesfrom the element.
10What are the ways to render conditionally, and what is the && pitfall?Junior
An early return, a ternary, or &&. They are plain JavaScript, since JSX is just expressions. The pitfall: && returns its left operand when that is falsy, and React renders the number 0, so {items.length && <List />} prints a 0 on screen when the list is empty.
- React renders strings and numbers (including
0andNaN). It renders nothing forfalse,true,nullandundefined. - For more than two or three cases, map the state to a component with an object lookup or a
switch, and avoid nested ternaries.
- How do you stop a component from rendering anything?
- Return
null. The component stays mounted, with its state and effects intact; it just outputs nothing. - What is the difference between returning
nulland not rendering the component at all? - If the parent stops rendering it, the component is unmounted and its state is destroyed. If you want to hide something and keep its state, keep it mounted and hide it with CSS, or use
<Activity mode="hidden">in React 19.2.
11What are fragments, and why use one instead of a div?Junior
A fragment groups several elements without adding a node to the DOM. A component must return a single root, and a wrapper div is not always harmless: it can break flex and grid layouts, produce invalid HTML inside tables and lists, and deepen the DOM for no reason.
A <div> between <tr> and <td>, or between <dl> and <dt>, is invalid HTML, and browsers will rearrange it. The fragment disappears at render time and leaves the correct structure.
12How does event handling work in React? What is a synthetic event?Mid
React wraps the browser's native event in a SyntheticEvent, an object with the same interface in every browser. It also uses event delegation: listeners are attached once at the root of the app, not on each element, and React dispatches to your handlers as the event bubbles.
- Since React 17, events are delegated to the root container, not
document, so several React roots (or React inside another app) do not interfere. - Event pooling was removed in React 17. You can read the event after an
awaitwithoutevent.persist(). event.nativeEventgives you the underlying browser event.- Events bubble through the React tree, which for portals is not the same as the DOM tree.
- How do you pass an argument to a handler?
- Close over it with an arrow function:
onClick={() => remove(item.id)}. The arrow is created each render, which is fine unless the child is memoised and that prop is what breaks the memoisation. - When do you need a native listener instead?
- For events on
windowordocument, and when you need{ passive: false }to callpreventDefaulton wheel or touch events. Add it in an effect with a cleanup.
13What are refs, and when should you use them?Mid
A ref is a mutable box, { current }, that persists for the lifetime of a component and does not trigger a re-render when it changes. It has two jobs: holding a DOM node so you can call imperative methods on it, and holding values the render output does not depend on, such as a timer ID.
- Use a ref for: focus, text selection, scrolling, measuring, media playback, and handing a DOM node to a non-React library.
- Do not use a ref for: anything you can express declaratively. If the screen should change, it is state.
- Do not read or write
ref.currentduring render. Do it in event handlers and effects. Render must stay pure.
To let a parent hold a ref to something inside your component, accept ref as a prop. In React 19 that is all it takes; earlier versions required wrapping the component in forwardRef. useImperativeHandle narrows what the parent gets from the raw DOM node to a small API of your choosing.
useRefvscreateRef?createRefreturns a new object every time it is called, so it suits class components, where it is called once in the constructor. In a function component it would reset on every render;useRefreturns the same object each time.- What is a callback ref?
- A function passed to
ref. React calls it with the node when it attaches and withnullwhen it detaches (or, in React 19, calls the cleanup function you returned). It is the tool for measuring a node as soon as it exists, or for collecting refs to a dynamic list. - Ref or state?
- If the value affects what is rendered, state. If it is bookkeeping that should survive renders without causing one, a ref.
14What does Strict Mode do? Why does my effect run twice?Mid
<StrictMode> is a development-only tool that deliberately stresses your components to expose bugs. It renders each component twice to catch impure renders, and on mount it runs every effect's setup, cleanup, then setup again to catch missing cleanups. Nothing is doubled in production.
- Double render: component bodies, state initialisers, updater functions and
useMemocallbacks run twice. If the result differs, your render has a side effect. - Double effect: setup, cleanup, setup. If the app misbehaves, an effect is not undoing what it did.
- Double ref callbacks, for the same reason.
- Deprecation warnings for legacy APIs.
Interview line: If Strict Mode breaks something, the bug was already there. The fix is to make the effect symmetrical, so setup followed by cleanup leaves no trace, not to remove Strict Mode or guard the effect with a ref.
- My API request fires twice in development. How do I fix it?
- Make the duplicate harmless: abort or ignore the first response in the cleanup. Better still, fetch with a library or framework that de-duplicates and caches requests. In production it fires once.
- Why would React ever mount, unmount and remount a component for real?
- Fast Refresh already does it in development, and features that hide and restore UI while keeping its state (such as
<Activity>) depend on effects being safe to tear down and re-run.
15What is a portal, and when do you need one?Mid
createPortal(children, domNode) renders children into a different part of the DOM while keeping them in the same place in the React tree. You need it when an element must visually escape its parent: modals, tooltips, dropdowns and toasts that would otherwise be clipped by overflow: hidden or trapped in a stacking context.
- Context still works: the portal's children read providers from where the portal is rendered in React, not where it lands in the DOM.
- Events bubble through the React tree. A click inside the modal reaches an
onClickon the component that rendered it, even though the DOM nodes are not nested. - On the server there is no
document, so a portal has to be rendered on the client only.
The trap: A portal solves positioning, not accessibility. A real modal also needs focus moved into it, focus trapped while it is open, focus restored on close, Escape to dismiss and the background made inert. The native <dialog> element and the Popover API now give you much of that for free.
16What are error boundaries? What do they not catch?Mid
An error boundary is a component that catches errors thrown while rendering anything below it, and shows a fallback instead of letting the whole app unmount. They catch errors in rendering, in lifecycle methods and in constructors. They do not catch errors in event handlers, asynchronous code, server rendering, or in the boundary itself.
- This is the one thing that still requires a class component. In practice most teams use the
react-error-boundarypackage, which wraps it with a reset API and hooks. - An error that no boundary catches unmounts the entire React tree. A blank page is judged safer than a UI in a corrupted state.
- Place boundaries at more than one level: one at the root as a last resort, one per route, and one around anything risky (a third-party widget, a chart, user-generated content).
- How do you let the user retry?
- Reset the boundary's state. The simplest way is to change its
key(for example a retry counter), which remounts it and its children.react-error-boundaryexposesresetErrorBoundaryandresetKeysfor this. - How do you get an async error into a boundary?
- Rethrow it during render: store it in state and throw on the next render, which is what
showBoundaryfromreact-error-boundarydoes. In React 19, errors thrown inside a transition or Action are routed to the nearest boundary automatically. try/catchvs an error boundary?try/catchis imperative and covers the synchronous code inside the block. A boundary is declarative and covers rendering of a whole subtree, including components you did not write.
17Class components vs function components: what changed, and do you ever still write a class?Junior
Function components with hooks are the standard. They are plain functions, share logic through custom hooks, and are the only kind that new features (Server Components, the use hook, Actions, the React Compiler) support. The single remaining reason to write a class is an error boundary.
The trap: The mapping is a translation aid, not a mental model. An effect is not 'the mount lifecycle'; it synchronises something outside React with the current props and state, and re-runs whenever those change. Thinking in lifecycles is where most stale-dependency bugs come from.
- Are function components faster?
- Not meaningfully. The reasons to prefer them are simpler code, no
this, logic reuse through hooks, and compatibility with everything React has shipped since 2019. - Do you need to rewrite existing class components?
- No. Classes are still supported and can be mixed freely with function components. Convert when you are changing the component anyway.
18What are the ways to style a React application?Junior
Plain CSS files, CSS Modules, utility classes such as Tailwind, CSS-in-JS, and inline styles. They differ in how styles are scoped, whether there is a runtime cost, and how dynamic values are handled.
Interview line: My default is whatever the codebase already uses. Starting fresh, I would pick CSS Modules or Tailwind: both are static CSS with no runtime and work with server rendering.
Want reps on this? Drill Rendering & Keys →
Hooks
The centre of any modern React interview. Expect to explain what each hook is for, predict what a snippet logs, and find the bug in an effect.
01What are hooks, and what problems did they solve?Junior
Hooks are functions that let a function component use React features: state, effects, context, refs. They arrived in React 16.8 to fix three problems with classes: stateful logic was hard to share, related code was scattered across lifecycle methods, and this was a constant source of bugs.
- Reuse. Sharing stateful logic used to mean higher-order components or render props, both of which wrap your component in more components. A custom hook is just a function call.
- Cohesion. A subscription was set up in
componentDidMount, updated incomponentDidUpdateand torn down incomponentWillUnmount. With an effect, setup and cleanup sit together. - Simplicity. No
this, no binding handlers in the constructor, no class boilerplate.
- Do hooks replace Redux, HOCs and render props?
- They replace most uses of HOCs and render props.
useReducerplus context covers small global state, but a store library still earns its place when many components need selective subscriptions, middleware or devtools. - Can you use hooks in a class component?
- No. To use hook-based logic from a class, wrap the class in a function component that calls the hook and passes the result as props.
02What are the Rules of Hooks, and why do they exist?Junior
Call hooks only at the top level of a component or custom hook, never inside conditions, loops, nested functions or after an early return; and call them only from React functions. React identifies each hook by the order in which it is called, so that order has to be identical on every render.
React does not know the names of your state variables. For each component it keeps a list of hook slots, and on every render it hands out slot 1 to the first hook call, slot 2 to the second, and so on. Skip one call and every hook after it reads the wrong slot.
- The
useprefix is a naming convention with teeth: it is how the linter knows a function is a hook and must follow the rules. eslint-plugin-react-hooksenforces both rules (rules-of-hooks) and checks dependency arrays (exhaustive-deps).- The one exception is
use, which may be called inside conditions and loops.
- What actually happens if you break the rule?
- React throws 'Rendered fewer hooks than expected' (or more), or worse, silently pairs state with the wrong hook. The linter catches it before that.
- Why can you not call a hook in an event handler?
- A handler runs outside of rendering. Hooks only have meaning while React is rendering a component and can tell which component's slots are being filled.
03How does useState work? What are its gotchas?Junior
useState(initial) returns the current value and a setter. Calling the setter schedules a re-render with the new value. The state variable itself is a constant for that render: it never changes mid-function, which is the root of most useState confusion.
- The initial value is only used on the first render.
useState(buildInitialRows())still calls the function on every render and throws the result away. Pass the function itself to run it once. - Setting replaces; it does not merge. Unlike
this.setStatein classes, an object must be spread:setForm({ ...form, name }). - Same value, no render. If the new value is identical by
Object.is, React skips the update. That is why mutating an object and setting it again does nothing. - The setter is stable. Its identity never changes, so it is safe to omit from dependency arrays and to pass to memoised children.
- Updates are batched. Several setter calls in one event produce one re-render.
Interview line: State behaves like a snapshot. Every render gets its own copy of every state variable, and the handlers created in that render see that copy forever.
- One state object or several state variables?
- Separate variables for values that change independently; one object (or a reducer) for values that always change together. Avoid redundant or contradictory state: if it can be computed from other state, do not store it.
- When do you need the updater function form?
- Whenever the next state depends on the previous one and you cannot be sure the value in your closure is current: several updates in one handler, or updates from timers, subscriptions and async callbacks.
04What happens when you call a state setter three times in a row? Is setting state asynchronous?Mid
The setter does not change the variable; it queues an update and asks React to re-render. All updates queued during one event are batched into a single render. So three calls with count + 1 all compute from the same snapshot and the result is one increment, not three.
After the first click the console logs 0, the snapshot this handler closed over, and the button shows 1: three requests to set the value to 0 + 1.
- The update is added to a queue on that component.
- React schedules a render. Updates from the same event are batched together.
- During the render, React runs the queue in order to compute the new state, then calls the component.
- It diffs the result against the previous tree and commits the changes to the DOM.
- Layout effects run, the browser paints, then effects run.
The trap: 'Asynchronous' is a misleading word here. The setter returns nothing you can await. The value in the current render will never change no matter how long you wait, because it belongs to that render. The new value exists only in the next one.
- What changed with batching in React 18?
- Batching became automatic everywhere: in timeouts, promises and native event handlers, not just React event handlers. Before 18, two setters inside a
setTimeoutcaused two renders. - How do you run code after the state has been applied?
- If it needs the new value, compute that value into a local variable first and use it for both the setter and your logic. If it needs the updated DOM, use an effect that depends on the state. For the rare case where you need the DOM updated synchronously inside a handler,
flushSyncforces it.
05What is useEffect for, and how do the dependency array and cleanup work?Junior
useEffect synchronises a component with something outside React: a subscription, a timer, the document title, a network connection. The function runs after the render is committed to the screen. The function it returns is the cleanup, which runs before the effect runs again and when the component unmounts.
- The order on an update: render, commit to the DOM, browser paints, cleanup of the previous effect (with the previous props and state), then the new effect.
- Dependencies are not a choice. Every reactive value the effect reads (props, state, anything derived from them) belongs in the array. If you want fewer dependencies, change the code so the effect reads less.
- The effect function cannot be
async. It must return a cleanup function or nothing, not a promise. Declare an async function inside and call it. - Effects do not run on the server, and children's effects run before their parent's.
- How do you run something when a component unmounts?
- Return a cleanup function from an effect. With an empty dependency array it runs only on unmount; with dependencies it also runs before each re-run.
- How do you react to a change in one field of an object?
- Depend on that field, not on the object:
[user.id]. The object may be a new reference on every render even when the field has not changed. - Is
useEffect(fn, [])the same ascomponentDidMount? - Close, but it is the wrong way to think about it. The effect says 'keep this in sync with nothing', which happens to mean once. In development under Strict Mode it runs twice, to check that your cleanup works.
06Why does my effect run on every render, or loop forever?Mid
Because one of its dependencies is a new reference every render, or because the effect sets state that it also depends on. Objects, arrays and functions created during render are never equal to their previous versions, so React thinks they changed.
- Can you just leave the dependency out and silence the linter?
- You can, and you will ship a stale-closure bug: the effect keeps using values from the render in which it last ran. A suppressed
exhaustive-depswarning is almost always a real defect waiting for the right sequence of events.
07What is the difference between useEffect and useLayoutEffect?Mid
Timing. useEffect runs after the browser has painted, so it does not block the screen from updating. useLayoutEffect runs synchronously after React has updated the DOM but before the browser paints, so anything it changes is included in the first paint. Use it only when you must measure the DOM and adjust before the user sees a flicker.
- React renders and commits changes to the DOM.
useLayoutEffectcallbacks run. The DOM is updated but nothing is on screen yet.- The browser paints.
useEffectcallbacks run.
The trap: Because it blocks painting, slow code in a layout effect makes the whole UI feel sluggish. Default to useEffect and switch only when you can see the flicker you are fixing.
- What is
useInsertionEffect? - An even earlier hook that fires before layout effects, meant for CSS-in-JS libraries to inject style tags before anything reads layout. Application code should not need it.
- What about server rendering?
- Neither kind of effect runs on the server. A component that depends on a layout effect to look right will render its unmeasured state in the server HTML, so render that part on the client only or give it a sensible fallback.
08What is a stale closure in a hook, and how do you fix it?Mid
Every render creates new functions that close over that render's props and state. A stale closure is a function from an old render that is still running, in a timer, subscription or memoised callback, and still sees the old values. It happens when a dependency array claims the function needs nothing that it actually reads.
- A click handler starts a 3-second timeout that alerts
count. You click it, then increment twice. What does it show? - The value from when you clicked. The handler belongs to the render in which it was created and closed over that render's
count. That is the same mechanism, working as intended. - How does
useCallbackcause this? useCallback(fn, [])keeps the first render's function forever. Iffnreads state, it reads the first render's state. The dependency array must list everything the function uses.
09What is wrong with this component? (An effect that computes state from props.)Mid
The filtered list is derived data, and it is being stored in state and kept in sync with an effect. That costs an extra render with stale results on every change, and gives two sources of truth that can disagree. Compute it during render instead.
Interview line: Effects are for synchronising with systems outside React. If nothing external is involved, I probably do not need one.
- When is an effect the right tool?
- Subscriptions, timers, manual DOM work, analytics that should fire because the component was shown, and connecting to anything that is not React: a WebSocket, a map widget, the browser's media APIs.
10How do you fetch data in an effect correctly? What are the pitfalls?Mid
Fetch inside the effect, depend on the inputs of the request, and use the cleanup to cancel or ignore a response that is no longer wanted. Without that cleanup you have a race condition: a slow response for a previous input can arrive last and overwrite the current one.
- Race conditions.
userIdgoes 1 then 2; the response for 1 arrives after the response for 2. Without the abort, the screen shows user 1 under a URL for user 2. - Unchecked status.
fetchonly rejects on network failure. A 500 is a successful promise. - Missing states. Every request has loading, error, empty and success. Each needs a UI.
- No caching. Every mount refetches; two components asking for the same data make two requests.
- Waterfalls. A parent fetches, renders, and only then a child starts its own fetch.
- No server rendering. Effects do not run on the server, so the HTML arrives empty.
Interview line: I can write this by hand, and in production I would not. TanStack Query, SWR or RTK Query handle cancellation, caching, deduplication, retries and revalidation; a framework can fetch on the server so the data is in the first HTML.
- What if the API cannot be aborted?
- Use an
ignoreflag: setlet ignore = falsein the effect, check it before calling the setter, and set it totruein the cleanup. The request still completes; its result is discarded. - Where should a request go in a class component?
componentDidMount, and again incomponentDidUpdatewhen the relevant prop changed, with the same care about stale responses.
11useRef or useState? What is a ref good for besides the DOM?Mid
Both persist a value across renders. Changing state triggers a re-render; changing a ref does not. So state is for anything the screen depends on, and a ref is for bookkeeping that the render output never reads: timer IDs, the previous value, an 'is this the first run' flag, the latest version of a callback.
The classic small exercise is usePrevious, a ref updated after each render:
The trap: If you find yourself changing a ref and wondering why the screen did not update, it should have been state. And reading ref.current while rendering makes the output depend on something React cannot track, which is why the linter and the React Compiler flag it.
- How do you skip an effect on the first render?
- Keep a ref
isFirstRun, initialised totrue; in the effect, if it is true, set it to false and return. It is worth asking whether the logic belongs in an event handler instead, since 'on update but not on mount' usually means 'when the user did something'. - Why not a module-level variable?
- A module variable is shared by every instance of the component, and between requests on the server. A ref belongs to one mounted instance.
12What is the difference between useMemo, useCallback and React.memo?Mid
useMemo caches the result of a calculation between renders. useCallback caches a function's identity between renders. React.memo wraps a component so it skips re-rendering when its props are shallowly equal. The first two are mostly useful because of the third: they keep props stable so a memoised child can actually skip.
useCallback(fn, deps)is exactlyuseMemo(() => fn, deps).- Memoisation is not free: it costs memory, a dependency comparison on every render, and readability. An incorrect dependency array turns it into a stale-value bug.
useCallbackon a function passed to a non-memoised child does nothing useful. The child re-renders anyway.- The React Compiler inserts this memoisation automatically at build time. In a codebase that uses it, manual
useMemoanduseCallbackare mostly unnecessary.
Interview line: I do not memoise by default. I measure with the Profiler, and before reaching for memo I try the structural fixes: move state down, or pass the expensive part as children.
- A component wrapped in
memostill re-renders. Why? - A prop is a new reference each time (an inline object, array, arrow function or JSX
children), or the component reads a context that changed, or its own state changed.memoonly compares props, and only shallowly. - Is
useMemoa guarantee? - No, it is a performance hint. React may discard the cache, so the code must still be correct if the function runs again. Never rely on it for behaviour, such as running a side effect once.
13What is useReducer, and when would you use it instead of useState?Mid
useReducer moves state logic out of event handlers into one pure function, (state, action) => newState. Components dispatch actions describing what happened, and the reducer decides how state changes. Reach for it when several values change together, when the next state depends on the previous in non-trivial ways, or when many handlers update the same state.
- A reducer must be pure: no mutation, no requests, no timers. Same inputs, same output.
- Name actions after what happened (
"added","submitted"), not after the setter you would have called.
- How is this different from Redux?
- Same idea, smaller scope.
useReducerstate belongs to one component and dies with it; there is no store, middleware, devtools or selector subscription. Combined with context it can serve a subtree, at the cost of re-rendering every consumer on each change. - Is
dispatchstable? - Yes, like state setters. That makes it a good thing to pass through context: consumers that only dispatch never need to re-render.
14What is the Context API, and what are its pitfalls?Mid
Context makes a value available to every component below a provider without passing it through props at each level. Any descendant can read it with useContext. The catch: when the value changes, every component that reads that context re-renders, and there is no way to subscribe to part of it.
- Good for values that many components need and that change rarely: theme, locale, the signed-in user, feature flags, a store or client instance.
- Poor for state that changes often (the text of an input, a mouse position, a large frequently-updated object), because every consumer re-renders on each change.
Interview line: Context is a way to transport a value, not a state manager. The state still lives in a useState or useReducer somewhere; context only saves you the prop drilling.
- What is the difference between
<ThemeContext value>and<ThemeContext.Provider value>? - They do the same thing. React 19 lets you render the context object directly as the provider;
.Provideris the form from earlier versions. - Should you reach for context as soon as you see prop drilling?
- Try composition first. Often the intermediate components do not need to know about the data at all if the parent passes the finished element down as
childrenor as a prop.
15What is a custom hook? Write one.Junior
A custom hook is a function whose name starts with use and that calls other hooks. It extracts stateful logic so several components can share it. It shares the logic, not the state: each component that calls the hook gets its own independent state.
- The name must start with
use, so the linter can apply the Rules of Hooks to it and to its callers. - It can take any arguments and return anything: a value, a tuple, an object of values and functions.
- It follows the same rules as components: top-level calls only, same order every render.
- A function that calls no hooks is a plain utility. Do not give it a
usename.
Hooks worth being able to write from memory: useToggle, useDebouncedValue, usePrevious, useLocalStorage, useMediaQuery, useOnClickOutside, useEventListener, useInterval, useFetch.
- Two components call the same custom hook. Do they share state?
- No. Each call creates its own state and effects. To share the actual state, lift it up and pass it down, put it in context, or use an external store.
- When do you extract one?
- When the same effect-and-state combination appears in a second component, or when a component's intent is buried under plumbing and a name such as
useOnlineStatus()would make it readable. - How do you test a custom hook?
- With
renderHookfrom React Testing Library, wrapping state changes inact. Often it is better to test a small component that uses it, since that is how it runs for real.
16What is useId for?Mid
It generates a unique ID that is stable across the server render and the client hydration. Its job is wiring up accessibility attributes: htmlFor with id, aria-describedby, aria-labelledby. It is not for list keys.
- A hard-coded
id="password"breaks as soon as the component is rendered twice on one page. - A counter or
Math.random()produces different values on the server and the client, which is a hydration mismatch. - One
useIdcall can serve several related elements: add a suffix.
The trap: Do not use it as a key. Keys must come from your data so they identify the item, not the position in the render.
17What do useTransition and useDeferredValue do, and how do they differ?Senior
Both mark an update as non-urgent, so React can keep the interface responsive: it renders the low-priority update in the background and abandons it if something more urgent, like a keystroke, comes in. useTransition wraps the code that sets the state. useDeferredValue wraps a value you receive and gives you a copy that lags behind.
- Neither is a debounce. There is no fixed delay: the work starts immediately and yields to urgent updates. On a fast device there is no visible lag at all.
- They help with slow rendering, not slow networks. To cut the number of requests, you still debounce.
- With Suspense, a transition keeps the already-visible content on screen instead of replacing it with a fallback while the next screen loads.
- The value of a controlled input cannot be set in a transition; typing must stay synchronous.
- Why does the slow child need to be memoised for
useDeferredValueto help? - When
querychanges, the parent re-renders urgently with the old deferred value. If the child is not memoised it re-renders right then, on the urgent path, which is exactly the work you were trying to defer. - What changed for transitions in React 19?
startTransitionaccepts async functions. Those are Actions: React tracks the pending state across theawait, and errors thrown inside go to the nearest error boundary.
18Which hooks do you rarely need, and what are they for?Senior
useSyncExternalStore subscribes to state that lives outside React. useImperativeHandle customises what a ref exposes. useInsertionEffect is for CSS-in-JS libraries. useDebugValue labels custom hooks in DevTools. useEffectEvent reads the latest props and state from inside an effect without making the effect depend on them.
useSyncExternalStore exists because of concurrent rendering. React can pause a render halfway; if an external store changes during the pause, different components could render with different versions of the data, a glitch called tearing. The hook guarantees a consistent snapshot, and its third argument provides the value for the server so hydration matches.
- Why not subscribe in a
useEffectand copy the value into state? - It works, with rough edges: the first render shows a default and then flips, a change between render and effect can be missed, and it can tear under concurrent rendering.
useSyncExternalStorehandles all three.
Want reps on this? Drill Hooks & Lifecycles →
Rendering and reconciliation
How React decides what to do when something changes. This is where intermediate interviews turn into senior ones: the questions stop being 'what is' and become 'why did this re-render'.
01What causes a component to re-render?Junior
Three things: its own state changed, its parent re-rendered, or a context it reads changed. Note what is missing: 'its props changed'. A component re-renders when its parent does, whether or not the props are different, unless it is wrapped in memo.
- State change. Calling a setter (or
dispatch) with a different value queues a render of that component. - Parent render. Rendering is recursive by default: when a component renders, React renders everything it returns.
- Context change. Every component that reads the context re-renders when the provider's value changes, even through a memoised ancestor.
- External store. A subscription made with
useSyncExternalStore(which is what store libraries use) triggers a render when its snapshot changes.
The trap: 'Re-render' does not mean 'DOM update'. Rendering is React calling your function and comparing the result with last time. If nothing differs, the DOM is not touched. Most re-renders are cheap, and chasing every one of them is the usual premature optimisation.
- Setting state to the value it already has: does that re-render?
- No. React compares with
Object.isand bails out. (It may call the component once more before bailing out, but it will not render the children or run effects.) - How do you find out why a component rendered?
- React DevTools Profiler, with 'Record why each component rendered' enabled. It names the state, prop, hook or parent responsible.
02What are the render phase and the commit phase? Why must rendering be pure?Mid
In the render phase React calls your components and works out what changed; nothing on screen is touched, and the work can be paused, restarted or thrown away. In the commit phase it applies the changes to the DOM in one synchronous step. Because rendering may run more than once or not be used at all, a component must not have side effects while rendering.
- Trigger. The initial mount, or a state update.
- Render. React calls the affected components, builds the new tree of elements, and diffs it against the previous one. Pure computation.
- Commit. React mutates the DOM, attaches refs, and runs layout effects. Synchronous and uninterruptible, so the user never sees a half-updated screen.
- Paint. The browser draws the result.
- Passive effects.
useEffectcleanups and setups run.
- Pure means: given the same props, state and context, return the same JSX, and change nothing that existed before the call.
- Strict Mode calls components twice in development precisely to make impurity visible.
- Concurrent rendering, Suspense and the React Compiler all assume purity. An impure component may work today and break when one of them is switched on.
- Where do side effects go, then?
- In event handlers when they are caused by a user action, and in effects when they are caused by the component being displayed. Never in the component body.
- Can a render be thrown away?
- Yes. If a more urgent update arrives while React is rendering a transition, the in-progress work is discarded and restarted. Nothing was committed, so the user sees no inconsistency, provided the render had no side effects.
03What is reconciliation? How does React's diffing algorithm work?Mid
Reconciliation is how React compares the new element tree with the previous one to find the smallest set of DOM changes. A general tree diff is O(n³), so React uses an O(n) heuristic built on two assumptions: elements of different types produce different trees, and keys identify which children are the same across renders.
Children are where keys matter. Insert an item at the top of an unkeyed list and React compares position by position: it rewrites every existing item and appends one at the end. With keys it sees that the old items simply moved down, and inserts one node.
Interview line: Reconciliation compares the previous and next element trees and computes the minimal DOM updates. It runs in linear time thanks to two heuristics: a different type means a different subtree, and keys give list items a stable identity.
- Is reconciliation the same as the virtual DOM?
- The virtual DOM is the data (trees of elements); reconciliation is the algorithm that compares them. Fiber is the implementation of that algorithm since React 16.
- Does the diff look at the real DOM?
- No. It compares the new elements against React's own record of what it rendered last time. That is why changing the DOM behind React's back leads to surprises.
04When does React keep a component's state, and when does it reset it?Mid
State belongs to a position in the tree. If the same component type is rendered at the same position on the next render, its state is preserved. If the type changes, the position changes, or the key changes, React treats it as a different component: the old one is unmounted and the new one starts fresh.
- It is the position in the rendered tree that counts, not where the JSX appears in your source. Case 1 has two JSX tags and one position.
- A
keyis part of the position. Changing it is the supported way to reset a component on purpose:<Form key={recordId} />. - Removing a component from the tree destroys its state. To keep it, lift the state up, keep the component mounted and hide it, or use
<Activity mode="hidden">.
- A chat app keeps the draft message when you switch contacts. Why, and what is the fix?
- The
<Chat>component is the same type at the same position, so its state is preserved while only thecontactprop changes. Give itkey={contact.id}and each conversation gets its own state.
05In what order do a parent and child render and run their effects?Mid
Rendering goes top-down: parent, then child. Effects go bottom-up: the child's effects run before the parent's. On an update, every cleanup from the previous render runs before any of the new effects.
On mount, without Strict Mode:
After one click:
- Layout effects run during the commit, before the browser paints. Passive effects (
useEffect) run after. - A cleanup always sees the props and state of the render it was created in.
- Under Strict Mode in development, each render line appears twice and the mount shows an extra setup, cleanup, setup cycle.
- Why do children's effects run first?
- By the time a parent's effect runs, its whole subtree is mounted and its children's effects have completed, so the parent can safely interact with them (measure them, call methods through refs).
06What is React Fiber?Senior
Fiber is the reconciliation engine React has used since version 16, and also the name of its data structure. Each component instance and DOM node is a fiber: a unit of work linked to its parent, first child and next sibling. Because the tree is a linked list rather than a recursive call stack, React can pause work, resume it, prioritise it or throw it away.
- Before Fiber the reconciler recursed through the whole tree in one synchronous pass. A large update blocked the main thread until it finished, and input and animations stalled.
- A fiber holds the component type, its props, its state (the list of hooks), pointers to
child,siblingandreturn, and flags describing what must be done at commit. - Two trees.
currentis what is on screen;workInProgressis being built. On commit React swaps them. If a render is abandoned, the work-in-progress tree is simply discarded. - Priorities. Each update is assigned a lane. A keystroke outranks a transition, so React can interrupt the transition's render to handle the keystroke.
Interview line: The stack reconciler could not stop once it started. Fiber turned rendering into a loop over units of work that React controls, which is what allows it to schedule, interrupt and resume. The commit phase is still synchronous, so the DOM is never left half-updated.
- Is Fiber something you use directly?
- No, it is internal. You see it indirectly: hooks are stored on the fiber in call order (hence the Rules of Hooks), and DevTools shows the fiber tree.
07What is concurrent rendering?Senior
Since React 18, rendering can be interrupted. React can start rendering an update, pause to handle something more urgent, and then continue or start over. It is not a mode you switch on for the whole app; it is used when you opt in through features such as transitions, deferred values and Suspense.
- Urgent updates (typing, clicking, pressing) render synchronously, as always.
- Transitions (
startTransition,useDeferredValue) render in the background in small slices, yielding to the browser between them. - The commit is still atomic. However the render was scheduled, the DOM is updated in one go.
The trap: Concurrency raises the cost of impurity. A render that is started twice or discarded must not have done anything observable. External mutable state read during render can also change between slices, which is why stores need useSyncExternalStore.
- How do you enable it?
- By creating the root with
createRoot(orhydrateRoot), which is the only option since React 19. From there it is feature by feature: an update is concurrent only if it is marked as a transition or deferred. - Is it the same as the old 'Concurrent Mode'?
- That was the experimental all-or-nothing version. It was replaced by this opt-in model so existing apps could upgrade without every component having to be concurrency-safe at once.
08What is the difference between react and react-dom? How does an app start?Junior
react is the core: components, elements, hooks. It knows nothing about browsers. react-dom is a renderer that turns React's output into DOM operations. Splitting them lets the same core drive other renderers, such as React Native.
createRootstarts a client-rendered app in an empty container.hydrateRootattaches React to HTML that was already rendered on the server.ReactDOM.renderandReactDOM.hydrate, which you will see in older code, were removed in React 19.
Want reps on this? Drill Rendering & Keys →
Performance
Every 'how would you optimise this' question is really asking whether you measure before you fix, and whether you know more than one tool.
01A React app feels slow. How do you approach it?Mid
Measure first, in a production build, to find out which kind of slow it is: too much JavaScript to load, too many re-renders, renders that are individually expensive, or waiting on the network. Each has different fixes, and guessing usually optimises the wrong thing.
- Reproduce and measure. React DevTools Profiler shows which components rendered, why, and for how long. The browser's Performance panel shows long tasks. Lighthouse and field data show load metrics.
- Classify the problem. Slow to load, slow to respond to input, or janky while scrolling or animating.
- Fix the biggest thing. One change at a time, re-measuring after each.
- Guard it. A bundle-size budget or a performance test in CI so it does not regress.
The trap: Development builds are several times slower than production and render everything twice under Strict Mode. Never draw conclusions from profiling a dev build.
- How do you reduce bundle size?
- Analyse it first with a bundle visualiser. Then split by route, lazy-load heavy and rarely-used components, replace large dependencies (a date library where
Intlwould do, a utility library imported whole), make sure imports are tree-shakeable, and keep server-only code on the server. - Which optimisation do you reach for last?
- Wrapping things in
memo,useMemoanduseCallback. They add complexity and can hide the structural problem. Restructuring where state lives usually removes the re-render instead of making it cheaper.
02How do you stop an expensive subtree from re-rendering, without memo?Mid
Change the structure so the state that changes and the subtree that is expensive are no longer in the same component. Either move the state down into a smaller component, or lift the expensive content up and pass it in as children.
Fix 2 works because children is an element that Page created. When ThemePanel re-renders, it receives the very same element object as before, and React skips a subtree whose element has not changed.
- Keep state as close as possible to where it is used.
- Split a component that mixes fast-changing state with slow-rendering content.
- Keep unrelated state out of context: every consumer re-renders when it changes.
03What is React.memo? What are pure components, and when does memoisation fail?Mid
memo wraps a component so that React skips re-rendering it when the parent re-renders with shallowly equal props. It is the function-component version of the class PureComponent. It fails whenever a prop is a new reference each time, and it never blocks updates from the component's own state or context.
- The comparison is shallow: each prop is compared with
Object.is. Mutating an object in place is therefore invisible to it. memo(Component, arePropsEqual)accepts a custom comparison. It is rarely worth it, and an incorrect one causes stale UI.- Memoising costs a comparison on every render. For a cheap component that almost always receives new props, it is pure overhead.
- What was
shouldComponentUpdate? - The class method that let a component decide whether to re-render.
PureComponentimplemented it as a shallow comparison of props and state.memois the same idea for props of function components. - Should you wrap everything in
memo? - No. Use it where the Profiler shows a component that renders often, with the same props, and is slow to render. With the React Compiler enabled, this is done for you.
04What are code splitting and lazy loading? How do React.lazy and Suspense work together?Mid
Code splitting breaks the bundle into chunks that are downloaded on demand, so the first page load only pays for the code it needs. lazy wraps a dynamic import() in a component; the first time it renders, React suspends, shows the nearest <Suspense> fallback while the chunk downloads, then renders the real component.
- Declare
lazycomponents at module level. Inside a component, a new lazy component is created on every render and its state resets. - Wrap lazy boundaries in an error boundary: a chunk can fail to load after a deploy or on a bad connection.
- Preload on intent (hover, focus, route prefetch) so the chunk is usually there before the click.
- Place Suspense boundaries deliberately. One at the root gives a blank page; one per widget gives a pop-in of spinners.
- Does
lazysupport named exports? - Only default exports directly. Either re-export the component as a default from its own file, or map it in the promise as above.
- How is this different with Server Components?
- Server Components never ship to the browser at all, so their code is not in any chunk. Splitting happens automatically at each
"use client"boundary.
05How do you render a list of 100,000 rows?Mid
You do not render 100,000 rows. Virtualisation (or windowing) renders only the rows visible in the scroll viewport plus a small buffer, positions them absolutely inside a spacer as tall as the full list, and swaps rows in and out as the user scrolls.
- The DOM holds a few dozen nodes no matter how long the list is, so memory and layout cost stay flat.
- In production use a library: TanStack Virtual,
react-windoworreact-virtuoso. Variable row heights, sticky headers and scroll restoration are harder than they look.
- When is virtualisation the wrong answer?
- When the list is a few hundred simple rows, or when pagination or 'load more' matches how people use the data. CSS
content-visibility: autois a cheaper middle ground: the nodes exist, but the browser skips rendering the off-screen ones. - How do you combine it with infinite scroll?
- Fetch pages with cursor-based pagination as the user approaches the end (an
IntersectionObserveron a sentinel row), append them to the data, and let the virtualiser render only the visible window of the growing list.
06A context update is re-rendering half the app. How do you fix it?Senior
Every consumer of a context re-renders when its value changes, so reduce how often the value changes and how many components consume each piece. Memoise the value, split one large context into several by how often each part changes, and for high-frequency or fine-grained state move to a store with selectors.
- Memoise the value if it is an object, so the provider re-rendering does not by itself notify every consumer.
- Split by update frequency. State in one context,
dispatch(stable) in another; the user in one, the theme in another. - Push the read down. Have a small leaf component read the context, so the re-render is cheap.
- Use a store (Zustand, Redux, Jotai) when components need to subscribe to a slice. Context has no selectors.
The trap: memo on a consumer does not help: a context change re-renders the component from the inside, not through its props.
07How do you handle an expensive computation without freezing the UI?Senior
First avoid repeating it (memoise, or compute it on the server). If it still takes long, make it interruptible with a transition so input stays responsive. If it is pure CPU work unrelated to rendering, move it off the main thread into a Web Worker.
Interview line: My order of preference is: do not do the work, do it once, do it somewhere else, and only then do it in slices. A transition keeps the UI responsive while React renders; it does not make a 500 ms function shorter.
08What is the difference between debouncing and throttling? Implement a debounce.Mid
Debouncing waits until the calls stop: it runs once, after a quiet period. Throttling runs at most once per interval while the calls continue. Debounce a search box; throttle a scroll or resize handler.
In a component, the debounced function must be the same one across renders, and it must be cancelled on unmount:
The trap: Calling debounce(...) directly in the component body creates a new debounced function, with a new timer, on every render. Nothing is ever cancelled, and every keystroke fires.
- Debounce or
useDeferredValuefor a search field? - They solve different problems. Debouncing reduces how many requests you send.
useDeferredValuekeeps typing responsive while React renders a slow result list. A search box that hits an API usually wants the debounce, and may want both. - What else does a good typeahead need?
- Cancellation of the previous request, a minimum query length, a cache keyed by query, keyboard navigation, and the ARIA combobox roles so it works with a screen reader.
09What is the React Compiler?Senior
A build-time tool that analyses your components and inserts memoisation automatically, doing the job of useMemo, useCallback and React.memo at a finer granularity than people write by hand. It works only on code that follows the Rules of React, and skips components it cannot prove are safe.
- It runs as a plugin in the build (Babel, with integrations for Vite and Next.js). There is nothing to call at runtime.
- It assumes components are pure, props and state are not mutated, and hooks are called unconditionally. Code that breaks those rules is left uncompiled, not miscompiled.
- Its ESLint rules surface the violations, which is useful even before you enable the compiler.
- A
"use no memo"directive opts a single component or hook out while you investigate a problem.
The trap: It makes re-renders cheaper; it does not make structural problems go away. State that lives too high, a context that changes constantly, an unvirtualised list of 10,000 rows and a slow first render are all still yours to fix.
- Do you delete existing
useMemoanduseCallbackcalls? - There is no need to: the compiler works alongside them. New code can simply be written without them, and old ones can be removed over time where the compiler covers the case.
- Does that make memoisation knowledge obsolete?
- No. You still need to understand referential equality to read a Profiler trace, to work in codebases without the compiler, and to explain why something re-rendered.
10What are Core Web Vitals, and what can a React developer do about them?Senior
Three field metrics Google uses to describe user experience: LCP (how fast the main content appears), INP (how quickly the page responds to interactions) and CLS (how much the layout jumps around). Each maps to specific decisions in a React app.
- INP replaced First Input Delay in 2024. It looks at every interaction during the visit, not just the first, so slow re-renders now show up directly in the metric.
- A client-only React app typically has poor LCP: the browser must download, parse and run the bundle before anything meaningful is drawn.
- Hydration is main-thread work that competes with the user's first interactions. Server Components and code splitting reduce how much there is to hydrate.
- Measure in the field (real user monitoring, the
web-vitalslibrary), not only in Lighthouse on a fast laptop.
- A skeleton screen or a spinner?
- A skeleton that matches the final layout, because it reserves the space and so prevents layout shift, and it makes the wait feel shorter. Spinners are fine inside a button or a small region.
Want reps on this? Drill Vercel React Performance →
State management and patterns
Design questions. There is rarely one right answer, so what is being assessed is whether you can name the trade-offs and say when you would choose differently.
01Local state, context, or a store: how do you decide where state lives?Mid
Start by asking what kind of state it is. Most of what people put in a global store is really server data that belongs in a cache, or URL state that belongs in the address bar. For genuine client state, keep it as local as possible and move it outward only when something forces you to.
- Can it be computed from existing props or state? Then it is not state.
- Does only one component need it? Keep it there.
- Do a few nearby components need it? Lift it to their common parent.
- Should it survive a refresh or be shareable as a link? Put it in the URL.
- Does it come from the server? Let a query cache own it.
- Is it needed all over the tree? Context if it changes rarely, a store if it changes often.
Interview line: I reach for a store last. Once server data moves into a query cache and filters move into the URL, what is left is usually small enough for local state and one or two contexts.
- Why not put everything in one global store?
- Everything becomes coupled to the store, components stop being reusable, state outlives the screen that needed it, and you rebuild by hand what a query library already does: caching, deduplication, invalidation, loading and error states.
- Why is the URL a good place for state?
- It survives a refresh, it works with the back button, it can be shared or bookmarked, and the server can read it for the first render. Filters kept in
useStatehave none of that.
02What is prop drilling, and how do you avoid it?Junior
Prop drilling is passing data through components that do not use it, just to get it to a deeply nested one. A little is fine and explicit. When it hurts, the first fix is composition, so the intermediate components never see the data; then context; then a store.
- Is prop drilling always bad?
- No. Two or three levels of explicit props are easier to trace than context, because you can follow the data by reading the code. It becomes a problem when adding one prop means editing five files that do not care about it.
03What is Redux, and how does it work? When would you not use it?Mid
Redux keeps application state in a single store outside the component tree. The only way to change it is to dispatch an action, a plain object describing what happened; pure functions called reducers compute the next state from the current state and the action. Components subscribe to the slices they need through selectors.
- Single source of truth. One store holds the state tree.
- State is read-only. You never assign to it; you dispatch actions.
- Changes are made by pure reducers.
(state, action) => newState, with no side effects, which is what makes time-travel debugging and replay possible.
- The UI dispatches an action.
- Middleware sees it first (thunks, logging, async work).
- Reducers compute the next state.
- The store notifies subscribers.
- Selectors run; components whose selected value changed re-render.
The trap: Do not use Redux as a cache for API responses with hand-written loading flags. That is the part of Redux that made people dislike it, and RTK Query or TanStack Query replaces all of it.
- How does Redux relate to Flux?
- Flux is the pattern: actions flow one way through a dispatcher into stores, which update views. Redux is a refinement of it with one store, no dispatcher, and reducers as pure functions.
- How do you handle async logic?
- With middleware. Thunks (built into Redux Toolkit) are functions that receive
dispatchand can await things. Sagas use generators for complex orchestration such as cancellation and races. For plain data fetching, RTK Query generates the whole thing. - Can a reducer dispatch an action or call an API?
- No. A reducer must be pure. Side effects belong in middleware or in the event handler that dispatches.
- What were
connect,mapStateToPropsandmapDispatchToProps? - The pre-hooks API: a higher-order component that subscribed a class component to the store and injected state and action creators as props.
useSelectoranduseDispatchreplace them. - Redux vs MobX?
- Redux is explicit and immutable: plain actions, pure reducers, one store. MobX is implicit and mutable: you change observable objects directly and it tracks which components read what. MobX needs less code; Redux is easier to trace and debug.
04What is 'server state', and why use a library like TanStack Query for it?Mid
Server state is data your app does not own: it lives on a server, you hold a copy, and that copy can go stale at any time. Managing it well means caching, deduplicating requests, refetching in the background, retrying, and invalidating after mutations. A query library does all of that behind one hook.
The important idea is the split. Client state is synchronous and you are its only writer. Server state is asynchronous, shared, and can change without you. Treating them the same way is what led to stores full of isLoading flags.
- How do optimistic updates work with it?
- In
onMutate: cancel in-flight queries for that key, save the current cache value, and write the expected result into the cache. InonError, restore the saved value. InonSettled, invalidate so the server's truth replaces your guess. - What is the difference between
staleTimeandgcTime? staleTimeis how long data counts as fresh, during which no refetch happens.gcTimeis how long unused data stays in memory after the last component using it unmounts.- Do you still need this with Server Components?
- Less. Data fetched in a Server Component arrives with the HTML and the framework handles caching and revalidation. A client cache is still useful for highly interactive screens: polling, infinite scroll, optimistic mutations.
05What is a higher-order component? What are its drawbacks?Mid
A higher-order component is a function that takes a component and returns a new component with extra behaviour or props. It was the main way to share logic across class components. Custom hooks now cover almost every use, without the wrapper.
- Never apply a HOC inside a render function. It creates a new component type every time, so the subtree unmounts and remounts on each render.
- A HOC should pass through unrelated props, and must not mutate the component it receives.
- You still meet them in older code and some library APIs:
connectfrom React Redux,withRouter,React.memoitself.
Interview line: The hook equivalent of withAuth is const { user } = useAuth() inside the component. Same logic, explicit at the call site, no extra component in the tree.
- Do hooks replace HOCs completely?
- Nearly. A HOC can still be the right shape when you need to wrap rendering itself, for example to put a component behind an error boundary, a permission check or a feature flag without touching it.
06What is the render props pattern?Mid
A component that takes a function as a prop and calls it to decide what to render, passing its internal state as arguments. The component owns the behaviour; the caller owns the markup. The function is often passed as children.
- The prop does not have to be called
render. Any prop that is a function returning JSX is a render prop. - Like HOCs, it predates hooks.
const position = useMousePosition()is simpler when you only need the data. - It is still the right tool when the component must control where and how often your markup is rendered: virtualised lists (
renderItem), headless UI components, form field wrappers.
The trap: Several render-prop components nested inside each other produce a pyramid of callbacks. That, more than anything, is what hooks were designed to flatten.
07What does 'composition over inheritance' mean in React? What is a compound component?Mid
React components are combined, not extended. You build specific components by rendering generic ones and passing them content through children or other props. A compound component is a set of components designed to be used together that share state implicitly, like <select> and <option> do.
- The caller controls structure and order; the parent owns the state. Compare that with one
<Tabs items={[...]} />component that needs a new prop for every variation. - This is the cure for boolean-prop explosion (
showHeader,withIcon,compact...): expose pieces to compose, not flags to combine.
- Why does React not use inheritance for components?
- Props and composition already cover every case inheritance would: a component can accept arbitrary elements, functions and data. Shared non-UI logic goes in hooks or plain modules. The React team has said they never found a case where a component hierarchy was the better design.
08What is the container and presentational component pattern? Is it still relevant?Mid
It splits components into two roles. Presentational components take props and render markup, with no knowledge of where data comes from. Container components fetch data and hold state, and render presentational components. The idea of separating concerns still holds; the mechanism today is usually a custom hook, not a wrapper component.
- Still valuable: components that only render are reusable, easy to test, and usable in a design system.
- No longer a rule: Dan Abramov, who popularised the pattern, later said not to split components this way dogmatically. Hooks let one component use data logic without a wrapper.
- The modern form of the same split is Server Components that fetch, rendering Client Components that interact.
09What React anti-patterns do you look for in a code review?Mid
Mostly state that should not be state, effects that should not be effects, and identity problems: unstable keys, components defined inside components, and objects recreated on every render where a stable reference is expected.
The trap: The older lists you will find online also mention componentWillMount, componentWillReceiveProps, string refs and findDOMNode. Those APIs are deprecated or removed, so they are history questions now, not review comments.
- Which of these causes the most bugs in practice?
- Derived state kept in sync with effects. It is the root of most 'the UI shows the old value for a moment' reports, and removing the state usually deletes more code than it adds.
10How do you handle forms and validation in React?Mid
For a small form, controlled inputs with validation in the submit handler, or uncontrolled inputs read from FormData. For anything larger, a form library (React Hook Form is the usual choice) with a schema (Zod) that produces both the validation and the TypeScript type. In React 19, form actions handle pending and error state without client-side plumbing.
- Validate on the server too. Client-side validation is for the user's convenience; it protects nothing.
- When to validate: on submit first, then on blur or on change for fields that already have an error. Validating on every keystroke from the start shouts at people mid-word.
- Accessibility: every input has a label; errors are linked with
aria-describedbyand announced; focus moves to the first invalid field on submit. - Performance: controlled inputs re-render the form on each keystroke. React Hook Form registers uncontrolled inputs, so typing in one field does not re-render the others.
- How do you prevent a double submit?
- Disable the submit button while the request is pending (
isSubmitting,useFormStatus().pending), and make the server operation idempotent, because disabling a button is not a guarantee. - How do you show errors that come back from the server?
- Map them onto the fields (
setErrorin React Hook Form, or the state returned by the action), and keep a form-level message for errors that belong to no field.
11How do you structure a large React codebase?Senior
By feature, not by file type. Code that changes together lives together, each feature exposes a small public API, and dependencies point in one direction: features may use shared code, shared code never imports from a feature. The exact folder names matter far less than enforcing those boundaries.
- Colocation. A component's styles, tests and stories sit next to it. Deleting a feature is deleting a folder.
- A public API per feature. Other code imports from
features/checkout, never from deep inside it. A lint rule (no-restricted-imports,eslint-plugin-boundaries) makes that a build error rather than a convention. - One-way dependencies.
appdepends onfeatures, which depend onshared. Two features that need each other usually share a concept that belongs in a third place. - Shared is earned. Code moves to
sharedwhen a second feature needs it, not in anticipation.
Named methodologies describe the same ideas at different granularity: Feature-Sliced Design formalises the layers (app, pages, widgets, features, entities, shared), and Atomic Design organises the UI library itself (atoms, molecules, organisms).
- What goes wrong with grouping by type (
components/,hooks/,utils/)? - It scales with the number of files, not the number of features. One change touches five distant folders, nothing tells you what belongs together, and every folder becomes a place where anything can import anything.
- Are barrel files a problem?
- As a feature's public API, no. As a habit in every folder, they can pull unrelated modules into a bundle, slow down tooling and hide circular imports.
Want reps on this? Drill State Management Taxonomy →
Server rendering and modern React
React 18 and 19, Suspense, Server Components and hydration. Interviewers use these to see whether your knowledge stopped at hooks or kept up.
01What is the difference between CSR, SSR, SSG and ISR?Mid
They differ in where and when the HTML is produced. Client-side rendering builds the page in the browser. Server-side rendering builds it on the server for each request. Static generation builds it once at build time. Incremental static regeneration serves a static page and rebuilds it in the background when it is stale.
- With SSR, SSG and ISR the browser receives real HTML and can paint immediately.
- It then downloads the JavaScript for the page.
- React hydrates: it attaches event handlers to the existing HTML. Only now is the page interactive.
- Streaming SSR sends the HTML in chunks as each part becomes ready, so one slow query no longer holds back the whole page.
- Server Components change what is sent: components that never need interactivity render on the server and ship no JavaScript at all.
- Modern frameworks mix these per route, and even per component, so the real question is usually 'which parts of this page are static, which are per-request, and which are interactive'.
Interview line: I choose by asking two things about each page: does the content differ per user or per request, and how fresh must it be? Static when it can be, server-rendered when it must be, client-rendered for the parts that are purely interactive.
- SSR makes the page appear sooner. Does it make it interactive sooner?
- Not by itself. The HTML shows early, but nothing responds until the JavaScript has loaded and hydration finishes. That gap is why shipping less client JavaScript matters as much as rendering on the server.
- Why does CSR hurt SEO and performance metrics?
- The initial HTML is an empty shell. Crawlers that do not run JavaScript see nothing, and the largest contentful paint cannot happen until the bundle has downloaded, parsed and fetched its data.
02What is hydration? What causes a hydration mismatch, and how do you fix one?Mid
Hydration is React taking over HTML that was rendered on the server: it renders the components again in the browser, expects the output to match what is already in the DOM, and attaches event handlers to it. A mismatch happens when the server and the first client render produce different output.
The trap: suppressHydrationWarning hides the warning for one element's text and attributes; it does not repair anything. Use it for an unavoidable difference such as a timestamp, not to silence a real bug.
- What does React do when it finds a mismatch?
- It logs an error showing the difference and falls back to client-rendering that subtree up to the nearest Suspense boundary, throwing away the server HTML there. You lose the benefit of server rendering for that part.
- What is selective hydration?
- With Suspense boundaries and streaming, React hydrates the page in pieces instead of all at once, and prioritises the piece the user is interacting with. Wrapping independent sections in Suspense is what makes it possible.
03What is Suspense?Mid
<Suspense> lets a component declare that it is not ready to render yet, and lets a parent decide what to show in the meantime. While anything inside the boundary is waiting, for lazily loaded code or for data, React shows the fallback; when it is ready, React swaps in the real content.
- Loading UI becomes declarative. No
isLoadingflags threaded through props; the boundary's position decides what is replaced by a placeholder. - Boundaries are a design decision. Things that should appear together share a boundary; things that can pop in independently get their own.
- Pair it with an error boundary. Suspense handles 'not yet'; an error boundary handles 'failed'.
- Transitions avoid flashes. If an update inside a transition suspends, React keeps showing the current screen instead of dropping back to a fallback the user has already seen past.
The trap: A plain fetch inside useEffect does not trigger Suspense. The component has to suspend during render, which means use, lazy, or a library built for it.
- How does a component 'suspend'?
- It throws a promise during render (that is what
useandlazydo internally). React catches it at the nearest boundary, shows the fallback, and re-renders the component when the promise settles. - Where do you put boundaries?
- Around each part that has its own loading time and can make sense by itself: the route, then major panels. Too few and the whole page waits for the slowest part; too many and the page fills with spinners.
04What are React Server Components? How do they differ from Client Components, and from SSR?Senior
Server Components run only on the server, at build time or per request. They can be async, read databases and files directly, and none of their code is sent to the browser. Client Components, marked with "use client", are the ones that hold state, run effects and handle events. SSR is a different thing: it turns either kind into HTML for the first paint.
"use client"marks a boundary, not a single component: everything that file imports becomes part of the client bundle. Put it as far down the tree as you can.- Props passed from a Server Component to a Client Component cross the network, so they must be serialisable: plain data, promises and JSX are fine; functions are not, with the exception of Server Functions.
- Server Components need a framework and bundler that support them, such as Next.js or React Router.
Interview line: RSC and SSR answer different questions. SSR is about the first response being HTML. RSC is about which components need to exist in the browser at all. Client Components are still server-rendered to HTML; Server Components are never hydrated.
- A Server Component needs something interactive deep inside it, and that interactive part needs to wrap more server content. How?
- Pass the server content as
children. The Client Component receives already-rendered output and places it; it never imports the Server Component, so that code stays on the server. - What are the benefits?
- Smaller bundles, data fetching next to the data with no client-server waterfall, secrets that never leave the server, and less to hydrate. The costs are a more complex mental model and a dependency on a framework.
- Can you use context in a Server Component?
- No, context is a client feature. You render a Client Component provider near the root and pass server-rendered children through it.
05What changed in React 18 and React 19?Mid
React 18 introduced concurrent rendering and the features built on it: automatic batching, transitions and streaming server rendering with Suspense. React 19 built on that with Actions for async mutations, the use hook, stable Server Components, ref as a regular prop, and a round of removals of long-deprecated APIs.
- Removed in 19:
ReactDOM.renderandhydrate,findDOMNode, string refs, legacy context,propTypesanddefaultPropson function components, andreact-test-renderer/shallow. - Since 19: the React Compiler reached a stable release, and React 19.2 added
<Activity>for hiding UI while keeping its state, anduseEffectEvent.
- Which of these changes how you write everyday components?
- Automatic batching and
refas a prop are invisible improvements. Actions withuseActionStatereplace the hand-written loading, error and data state around a mutation. And the compiler removes most manual memoisation. - How would you upgrade an app from 17 or 18 to 19?
- Go to the latest 18.x first and clear every deprecation warning, since 19 removes what 18 warned about. Run the official codemods, replace
ReactDOM.renderwithcreateRoot, update the type packages, and test under Strict Mode.
06What are Actions in React 19? What do useActionState and useFormStatus do?Senior
An Action is an async function run inside a transition, typically to perform a mutation. React tracks its pending state, sends thrown errors to the nearest error boundary, and for forms resets the fields on success. useActionState wraps an action and gives you its latest result and pending flag; useFormStatus lets a child of a form read whether that form is submitting.
useActionStatereturns[state, action, isPending]. The third value is useful when the pending UI lives in the same component as the form.useFormStatusonly works in a component rendered inside the<form>, not in the component that renders the form.- The action can be a Server Function, in which case the form works before JavaScript has loaded.
- How is this different from calling an async function in
onSubmit? - Functionally you can build the same thing. Actions make the pending and error handling part of React, keep the UI responsive because the update is a transition, sequence overlapping submissions, and integrate with Suspense, optimistic updates and server functions.
- What happens to an error thrown inside an action?
- It propagates to the nearest error boundary. For expected failures such as validation, return them as state so the form can show them inline.
07What is an optimistic update? How does useOptimistic help?Senior
An optimistic update shows the expected result of an action immediately, before the server confirms it, and rolls back if the request fails. useOptimistic gives you a temporary copy of some state that you can update during an action; when the action finishes, React discards the temporary value and shows the real state again.
- The user acts. The UI updates immediately with the predicted result.
- The request goes out in the background.
- On success, the real data replaces the prediction, usually with no visible change.
- On failure, the prediction disappears and you tell the user what happened.
The trap: The rollback is automatic; the explanation is not. If the request fails and the item simply vanishes, the user assumes a bug. Always pair the rollback with a visible error and a way to retry.
- How was this done before
useOptimistic? - By hand: update local state, fire the request, and restore the previous value in the
catch. Or with a query library: write the predicted value into the cache inonMutate, restore it inonError, and refetch inonSettled. - What makes optimistic updates hard?
- Concurrent actions on the same item, temporary IDs that have to be swapped for server IDs, and keeping the prediction logic identical to what the server will do.
08What is the use hook, and how is it different from fetching in useEffect?Senior
use reads the value of a promise or a context during render. Given a pending promise it suspends the component, so the nearest <Suspense> shows its fallback, and when the promise resolves the component renders with the value. There is no loading state to manage, and unlike other hooks it can be called inside conditions and loops.
The trap: The promise must be stable across renders. Creating it inside the component that calls use produces a new promise on every render and suspends forever. Create it in a Server Component, a router loader or a cache, and pass it down.
use(SomeContext)reads context likeuseContext, but may appear after an early return or inside anif.- It cannot be called inside a
try/catchblock; errors are handled by error boundaries.
- Why pass a promise as a prop instead of awaiting it in the parent?
- Awaiting in the parent blocks the parent's render until the data arrives. Passing the promise lets the parent render and stream immediately, while only the child that needs the data waits.
09What are Server Functions (Server Actions), and what must you be careful about?Senior
A Server Function is an async function marked with "use server" that runs on the server but can be called from client code as if it were local. The framework turns the call into a network request. Used as a form action or inside a transition it is called a Server Action. The danger is forgetting that it is a public HTTP endpoint.
- It is reachable by anyone. A direct POST can call it with any arguments, regardless of whether your UI shows the button. Check authentication and authorisation inside every function.
- TypeScript types are not validation. The argument typed
stringcan be anything at runtime. Parse it with a schema. - Do not trust what the client sends back. Hidden form fields and values bound from a previous render can be tampered with.
- Return only what the client may see. The return value is serialised to the browser.
On the positive side: there is no API route to write, the types flow end to end, and a <form action={serverFunction}> works before the page's JavaScript has loaded.
- When would you still write a regular API endpoint?
- When something other than your own React app calls it: a mobile client, a webhook, a public API. Server Functions are an implementation detail of one app, with no stable URL or contract.
10What does Next.js add on top of React? How do the Pages Router and App Router differ?Mid
Next.js is a framework around React: file-based routing, server rendering and static generation, data fetching and caching, code splitting, image and font optimisation, and API endpoints. The Pages Router is the original model, built on per-page data functions. The App Router is built on Server Components, nested layouts and streaming.
- Special files define UI for a route segment:
page,layout,loading(a Suspense fallback),error(an error boundary),not-found. - A route is static unless it uses something only known at request time, such as cookies, headers or uncached data.
- Mutations go through Server Functions;
next/linkprefetches routes;next/imageresizes and lazy-loads images.
The trap: Next.js changes quickly between major versions: caching defaults, async request APIs, the rename of Middleware to Proxy. In an interview, say which version you have used, and reason from the model (what is static, what is per-request, what runs on the client) instead of reciting an API from memory.
- When would you not use a framework?
- For an app that is entirely behind a login, has no SEO needs and is hosted as static files, a client-only Vite app is simpler to build and operate.
- What are the alternatives?
- React Router in framework mode (the successor to Remix), TanStack Start, and Astro for content-heavy sites. They make different choices about caching and where the server runs, on the same React foundations.
Want reps on this? Drill App Router & RSC →
Routing, testing and production concerns
Everything around the components: getting between pages, proving the code works, and keeping it secure and usable. Real projects are mostly this, and interviewers know it.
01How does client-side routing work? Walk through React Router's main pieces.Mid
A client-side router intercepts navigation, updates the URL with the History API without a page reload, and renders the components that match the new URL. React Router gives you route definitions, links, nested layouts, and hooks to read params and navigate in code.
BrowserRouteruses real paths (/users/42) through the History API, so the server must return the app for every route.HashRouterkeeps the route after a#, which works on any static host but gives uglier URLs and is not sent to the server.navigate(to)pushes a history entry;navigate(to, { replace: true })replaces the current one, so Back does not return to it.- Data routers (
createBrowserRouter) add loaders and actions, so data is fetched in parallel with the route's code instead of after it renders.
- How is this different from traditional server routing?
- Traditionally every navigation is a request for a new HTML document. A client router keeps one document, swaps components, and fetches only data. Transitions are faster and state survives, but the first load is heavier and you take on scroll restoration, focus management and loading states.
- How do you pass data from one route to another?
- Through the URL when it should survive a refresh or be shareable: path params and search params. Through
navigate(to, { state })for transient data such as 'where did you come from'. Through a shared store or cache for anything larger. - How do you code-split by route?
- Wrap route components in
lazywith a Suspense boundary, or use the router'slazyroute option, which also loads the route's loader in parallel.
02How do you implement protected routes and redirect back after login?Mid
Wrap the private routes in a layout route that checks the auth state. If there is no user, it renders a redirect to the login page and records where the user was heading. After a successful login, navigate to that saved location with replace, so the login page does not stay in the history.
- Handle the loading state explicitly. On a refresh the session is unknown for a moment; redirecting during that moment bounces a signed-in user to the login page.
replaceon both redirects keeps Back from trapping the user between the guard and the login page.- Validate the return path. A
returnToparameter that accepts any URL is an open redirect, which phishing relies on.
The trap: A route guard is user experience, not security. All the JavaScript for the protected page is already in the user's browser, and anyone can call the API directly. The server must check authentication and authorisation on every request.
- How do you handle role-based access?
- The same guard with a role check, rendering a 'not authorised' page instead of a redirect when the user is signed in but lacks permission. Hide navigation the user cannot use, and still enforce everything on the server.
03How do you handle authentication in a single-page app? Where do you store the token?Senior
Prefer a session held in an HttpOnly, Secure, SameSite cookie, which JavaScript cannot read and so an XSS bug cannot steal. On load, ask the server who the user is and keep the answer in memory. Tokens in localStorage are convenient and readable by any script on the page.
- Short-lived access, long-lived refresh. The access credential expires in minutes; a refresh credential in an
HttpOnlycookie renews it silently. - Handle 401 centrally. One place in the HTTP client attempts a refresh, retries the request once, and signs the user out if that fails.
- Sign out everywhere. Clear server-side session state, and notify other tabs with a
storageevent orBroadcastChannel. - The three states matter. Rendering as if anonymous while the session is still being checked causes a flash of the login screen on every refresh.
- If cookies are sent automatically, what stops CSRF?
SameSite=LaxorStrictstops the browser attaching the cookie to cross-site requests, which covers most cases. For anything that changes state, add a CSRF token or check theOriginheader, and never mutate on a GET.- With server rendering, where does the check happen?
- On the server, per request: read the session cookie, and redirect before any HTML is produced. The client never renders a protected page for an anonymous user at all.
04What are the main security concerns in a React app? How does React help with XSS?Senior
Cross-site scripting is the main one. React escapes every value you render with {}, so user content is displayed as text and not executed. The protection ends wherever you bypass it: dangerouslySetInnerHTML, user-controlled URLs, and anything that writes to the DOM directly.
Interview line: The client is not a security boundary. Validation and permission checks in React exist for the user's convenience; the server has to repeat every one of them.
- Why is it called
dangerouslySetInnerHTML, and why the__htmlobject? - The awkward name and shape are deliberate friction: you cannot use it by accident, and it stands out in code review and in a search of the codebase.
- Is an environment variable in a React app secret?
- Not if it is exposed to the browser. Variables with a public prefix (
NEXT_PUBLIC_,VITE_) are inlined into the bundle at build time and readable by anyone.
05How do you test React components?Mid
With React Testing Library on top of a runner such as Vitest or Jest. You render the component, find elements the way a user would (by role, label and text), interact with them, and assert on what is displayed. You test behaviour, not implementation, so the tests survive refactoring.
- Query priority:
getByRolefirst, thengetByLabelText,getByPlaceholderText,getByText;getByTestIdonly as a last resort. If you cannot find an element by role or label, a screen reader user cannot either. userEventoverfireEvent: it simulates the full sequence of events a real interaction produces (focus, key down, input, key up).- What to test: what the user sees and can do. Not state variables, not which hooks were called, not component internals.
- Why not test implementation details?
- Such tests fail when you refactor without changing behaviour, and pass when behaviour breaks in a way they did not look at. They cost maintenance and give false confidence.
- What were shallow rendering and Enzyme?
- Enzyme rendered a component one level deep and let tests reach into its state and instance. It encouraged implementation-detail tests and was never updated for modern React.
react-test-rendereris deprecated as of React 19. Testing Library is the standard. - What is snapshot testing, and is it useful?
- It serialises rendered output to a file and fails when the output changes. It is cheap to write and catches unintended changes, but large snapshots are approved without being read. Keep them small and prefer explicit assertions.
06How do you test asynchronous behaviour, API calls, hooks, and components that need providers?Mid
Await findBy queries for things that appear later. Mock the network at the HTTP level with Mock Service Worker, so the component's real fetching code runs. Test a custom hook through renderHook. And wrap the component in the providers it needs with a small custom render function.
The trap: Mocking your own fetchUser function means the test never exercises the URL, the parsing or the error handling. Intercepting at the network layer tests all of it, and the same handlers can power local development.
- What is the
actwarning? - It means a state update happened in your test outside of
act, usually because something asynchronous finished after the test stopped waiting. The fix is to await the resulting change withfindByorwaitFor, not to wrap things inactuntil the warning stops. - How do you test a component connected to Redux?
- Render it inside a real
<Provider>with a real store created for that test and preloaded with the state you need. Mock stores test that an action was dispatched, which is an implementation detail.
07How do you make a React application accessible?Mid
Mostly by using the right HTML: real buttons, links, labels, headings and landmarks give you keyboard support and screen reader semantics for free. Then manage focus where React changes the page without a navigation, and reach for ARIA only to describe what HTML cannot.
- Semantics: one
<h1>, headings in order,<nav>,<main>, lists as lists, tables as tables. A fragment avoids wrapper divs that would break them. - Keyboard: everything reachable with Tab, operable with Enter or Space, with a visible focus ring. Never remove outlines without replacing them.
- Focus management: move focus into a dialog when it opens and back to the trigger when it closes; move focus to the new page's heading on client-side navigation.
- Dynamic updates: announce asynchronous results with a live region (
role="status"oraria-live). - Images: meaningful
alttext;alt=""for decoration. - Motion and colour: honour
prefers-reduced-motion; keep sufficient contrast; never signal with colour alone.
- What is the first rule of ARIA?
- Do not use ARIA if a native element does the job. ARIA changes what is announced, not how the element behaves:
role="button"on a div does not make it focusable or respond to the keyboard. - What does a client-side router break for accessibility?
- A full page load moves focus to the top and announces the new title. A client-side transition does neither unless you do it: update
document.title, and move focus or announce the change.
08How do you internationalise a React app?Mid
Keep every user-facing string out of the components, in message catalogues keyed by ID, one per locale. Use a library that implements ICU MessageFormat for plurals and interpolation, and the browser's Intl APIs for dates, numbers and currencies. Never build sentences by concatenating strings.
- Plurals and gender vary by language, which is why you use ICU messages and not
count === 1 ? ... : .... - Word order changes too, so translate whole sentences with placeholders, never fragments glued together.
- Layout: text expands (German is often a third longer than English), and right-to-left languages need
dir="rtl"and logical CSS properties (margin-inline-start). - Loading: ship only the active locale's catalogue; load others on demand.
- Locale source: the URL (
/pt/...) is best for SEO and sharing; fall back to a saved preference, thenAccept-Language.
The common libraries are react-intl (FormatJS), react-i18next, Lingui, and next-intl for Next.js. They differ in API, not in the underlying approach.
- What are the two ways of formatting in
react-intl? - Components (
<FormattedMessage>,<FormattedDate>) for text rendered as elements, and the imperativeintlobject fromuseIntl()(formatMessage,formatDate) for anywhere a string is required: placeholders,aria-label,document.title. - How do you catch untranslated strings?
- Lint against string literals in JSX, extract message IDs in CI and compare against each catalogue, and use pseudo-localisation in development to make hard-coded text and truncation visible.
09How do you handle loading and error states consistently across an app?Senior
Decide it once at the architecture level, not per component. Every asynchronous view has four states: loading, error, empty and success. Use Suspense boundaries for loading and error boundaries for failures at the route and widget level, give each error a way to retry, and report everything unexpected to a monitoring service.
- Empty is not an error. Zero results needs its own design, usually with a suggestion for what to do next.
- Skeletons over spinners for content regions: they reserve the layout and avoid shift.
- Do not swallow errors. An empty
catchhides the failure from the user and from you. - Report with context: the component stack, the route, the release version and the user's last actions.
- What does React 19 add for error reporting?
createRootacceptsonCaughtError,onUncaughtErrorandonRecoverableErrorcallbacks, giving one place to send errors to monitoring, whether or not a boundary caught them.- Toast or inline message?
- Inline when the error relates to something on screen that the user must fix or retry. A toast for the outcome of a background action. A toast alone is easy to miss and is gone when the user looks for it.
10How do you debug a React application?Junior
React DevTools for the component tree: inspect and edit props, state and hooks, and see what rendered and why. The browser's own tools for everything else: breakpoints in the Sources panel, the Network tab for requests, the Performance panel for long tasks.
- Reproduce it reliably, and reduce it to the smallest case.
- Locate it: is the data wrong (network, state), or is the rendering of correct data wrong?
- Inspect the component's actual props and state in DevTools instead of guessing.
- Explain the bug before fixing it. A fix you cannot explain is a coincidence.
- Add a test that fails without the fix.
The trap: console.log(state) right after a setter shows the old value, which sends people hunting for a bug that is not there. Log during render, or inspect the component in DevTools.
- A component renders with the wrong data. What do you check first?
- Where the data comes from. In DevTools, look at the component's props and state, then walk up to the owner that supplied them. Most often the state was mutated, a key is wrong, or an effect holds a stale closure.
Want reps on this? Drill Frontend System Design →
Class components and legacy APIs
You will not write these in new code, but plenty of production codebases still contain them, and plenty of interviewers still ask. Know them well enough to read, maintain and migrate.
01What are the lifecycle methods of a class component, and in what order do they run?Mid
A class component goes through mounting, updating and unmounting. The three you use constantly are componentDidMount (set up), componentDidUpdate (react to changed props or state) and componentWillUnmount (clean up). The rest exist for optimisation and rare edge cases.
The trap: componentWillMount, componentWillReceiveProps and componentWillUpdate were deprecated in 16.3 because they run in the render phase, which can be invoked more than once. They survive only under an UNSAFE_ prefix. If asked where to fetch data, the answer was always componentDidMount, never componentWillMount.
- How do these map to hooks?
componentDidMount,componentDidUpdateandcomponentWillUnmountcollapse into oneuseEffectwith a cleanup and a dependency array.shouldComponentUpdatebecomesReact.memo.getDerivedStateFromPropsbecomes computing during render. The snapshot and error methods have no hook equivalent.- Why is the constructor called only once?
- Because the instance is created once, on mount. Updates call
renderon the same instance. That is why state initialised from props in the constructor does not follow later prop changes.
02How does this.setState work in a class? Why pass it a function?Mid
setState shallow-merges the object you give it into the current state and schedules a re-render. Updates are batched, so this.state is not updated immediately. When the next state depends on the previous one, pass a function, which receives the up-to-date state and props.
- Never assign
this.state.count = 1. It changes the object without telling React, and a latersetStatemay overwrite it. The only place to assignthis.stateis the constructor or a class field. - The merge is shallow:
setState({ user: { name } })replaces the wholeuserobject. forceUpdate()re-renders without a state change. Needing it is a sign that something that should be state is not.
- What happens if you call
setStatein the constructor? - Nothing useful: the component is not mounted yet, and React warns. Initialise
this.statedirectly there. - Why should you not call
setStateincomponentWillUnmount? - The component is about to be destroyed and will never render again, so the update is meaningless.
03Why did event handlers in class components need .bind(this)?Junior
Because a method loses its this when it is passed around as a plain function. onClick={this.handleClick} hands React the function without its object, and since class bodies run in strict mode, this inside it is undefined when React calls it.
- This is plain JavaScript behaviour, not a React rule:
const fn = obj.method; fn()losesthisanywhere. - The class-field arrow became the standard fix, because it needs no constructor.
- Function components have no
this, so the whole category of bug is gone. It is one of the reasons hooks were introduced.
- What is the difference between
super()andsuper(props)? - Both run the parent constructor. Passing
propsmakesthis.propsavailable inside your own constructor. Without it,this.propsisundefineduntil the constructor returns, although React assigns it afterwards. - Is a constructor required?
- No. With class fields for state and arrow-function handlers, most class components never needed one.
04Which old React APIs are deprecated or removed, and what replaces them?Mid
React 18 replaced the root API, and React 19 removed most of what had been deprecated for years: ReactDOM.render, string refs, findDOMNode, legacy context, and propTypes and defaultProps on function components. Each has a direct modern equivalent.
Interview line: For an upgrade I go to the last 18 release first and fix every warning under Strict Mode, because 19 removes exactly what 18 warns about. Then I run the codemods and update the type packages.
- Why were string refs removed?
- They were tied to the component that happened to be rendering, which broke with render props and composition, could not be statically analysed, and forced React to track the current owner. Object and callback refs have none of those problems.
- What were mixins, and why did React abandon them?
- A way to merge shared methods into
createReactClasscomponents. They created implicit dependencies and name clashes, and did not work with ES classes. Higher-order components replaced them, and hooks replaced those.
05How would you convert a class component to a function component with hooks?Mid
Mechanically at first: state becomes useState or useReducer, instance fields become refs, lifecycle methods become effects grouped by what they synchronise, and this disappears. Then go back over it, because a faithful translation of lifecycle code usually reveals effects you do not need.
- Make sure it is covered by tests that exercise behaviour, so you can tell if you broke it.
- State. One
useStateper independent value, oruseReducerif they change together. Remember that the setter replaces wheresetStatemerged. - Instance fields (
this.timer,this.controller) becomeuseRef. - Lifecycle. Do not translate method by method. Group by concern: the subscribe in
componentDidMountand the unsubscribe incomponentWillUnmountare one effect. componentDidUpdatecomparisons (prevProps.id !== this.props.id) become the effect's dependency array.- Derived state. Anything computed in
getDerivedStateFromPropsor synced incomponentDidUpdateis usually just a calculation during render. - Handlers become plain functions. No binding.
- Leave error boundaries as classes. There is no hook for them.
Click Follow on one profile and switch to another within three seconds. The class alerts the new profile's name, which is a bug; the function alerts the one you clicked. Function components capture the values of the render they belong to. That is usually what you want, and it is also the source of stale closures when you do not.
The trap: A conversion is not required. Class components are fully supported, and a working one can stay as it is. Convert when you are changing the component anyway, or when you need a hook-only feature inside it.
06What is the difference between createElement and cloneElement? What is the Children API?Mid
createElement(type, props, ...children) makes a new element; it is what JSX compiles to. cloneElement(element, props) copies an existing element with extra props merged in. Children is a set of helpers for inspecting and transforming the children prop. The last two are legacy patterns for letting a parent inject props into its children.
cloneElementmakes data flow invisible: the child gets a prop that appears nowhere in the JSX where the child is written.Children.maponly sees the elements passed directly. Wrap a child in another component or a fragment and the injection silently stops working.- The modern alternatives are context (the compound component pattern), a render prop, or simply passing the prop explicitly.
- When is
createElementstill useful? - When the element type is only known at runtime and you are not in a JSX file, for example in a library. In JSX the same is done with a capitalised variable:
const Tag = level === 1 ? "h1" : "h2"; return <Tag />.
Want reps on this? Drill Hooks & Lifecycles →
JavaScript under the hood
React and TypeScript interviews always dip into the language underneath. Closures explain hooks, the event loop explains effects and batching, and reference equality explains re-renders. Expect 'what does this log?'.
01What is a closure? What does this code log?Junior
A closure is a function together with the variables that were in scope where it was created. The function keeps access to those variables, live, not as copies, for as long as it exists, even after the outer function has returned.
read closes over the variable count, so it sees the current value. describe closes over label, which is a string computed once when count was 0. A closure captures variables, not expressions: nothing re-evaluates that concatenation.
var is function-scoped, so all three callbacks share one i, which is 3 by the time they run. let creates a new binding for every iteration, so each callback has its own j.
- Used for: private state (the counter above), function factories, callbacks and event handlers, memoisation, partial application.
- In React: every render creates new handler and effect functions that close over that render's props and state. That is why a handler sees the values from the render it was created in, and why a missing dependency produces a stale closure.
- Can closures cause memory leaks?
- They keep whatever they reference alive. A long-lived callback (a listener, an interval, a cache entry) that closes over a large object or a DOM node prevents it from being collected until the callback itself is released.
- How would you fix the
varloop withoutlet? - Create a new scope per iteration: an immediately invoked function that takes
ias a parameter, or passias the third argument tosetTimeout.
02How is this determined? What does this code log?Mid
For a regular function, this is decided by how the function is called, not where it is defined. For an arrow function, this is taken from the enclosing scope at the point of definition and can never be changed.
inner()is a plain call, so it has nothis, even though it is written inside a method.arrowhas nothisof its own; it usesgreet's, which isuser.- Destructuring
greetdetaches it fromuser. Called bare,thisisundefined, and reading.namethrows.
- How does this relate to React?
- It is the reason class component handlers had to be bound:
onClick={this.handleClick}passes the method detached from its instance. Function components do not usethisat all. - Can you re-bind an arrow function with
callorbind? - No. The first argument is ignored. An arrow's
thisis fixed lexically. - What does
bindreturn? - A new function permanently bound to the given
this(and optionally to leading arguments). Binding it again does not change it.
03Explain the event loop. In what order does this code log?Mid
JavaScript runs one thing at a time on a call stack. When the stack is empty, the event loop first runs every queued microtask (promise callbacks, queueMicrotask, the continuation after an await), and only then takes one macrotask (a timer, an I/O callback, a UI event). Then it repeats.
- Synchronous first:
A, thenF(an async function runs synchronously up to its firstawait), thenH. - Then all microtasks, in the order they were queued:
C,E,G. RunningCqueuesD, which goes to the back of the microtask queue and still runs before any timer. - Then one macrotask:
B, even though its delay was 0.
The microtask queue is drained completely after every macrotask. A microtask scheduled inside the first timer therefore runs before the second timer, and a microtask that keeps scheduling more microtasks can starve timers and rendering indefinitely.
- Is the function passed to
new Promise(...)asynchronous? - No. The executor runs synchronously, immediately. Only the
.thencallbacks are deferred. - Why does a long loop freeze the page?
- Rendering and input handling happen between tasks. While one task is running, nothing else can, including repainting. Break the work up, or move it to a Web Worker.
- Where does this show up in React?
- Batching: every state update in one task is collected into a single render. And timing: effects run after the browser has painted, layout effects before.
04How do promises work? What is the difference between Promise.all, allSettled, race and any?Mid
A promise represents a value that will exist later. It is pending, then settles exactly once, as fulfilled or rejected. async/await is syntax over the same thing. The four combinators differ in what they wait for and how they treat failure.
The trap: await inside a for loop runs the requests one after another. That is correct when each depends on the previous, and a threefold slowdown when they are independent.
- Does
Promise.allcancel the others when one rejects? - No. It rejects early, but the other operations keep running and their results are discarded. Promises cannot be cancelled; you cancel the underlying work with an
AbortController. - What happens to an error thrown in an
asyncfunction? - It becomes a rejection of the promise the function returned. If nothing awaits or catches it, you get an unhandled rejection.
- How do you add a timeout to a request?
fetch(url, { signal: AbortSignal.timeout(5000) }), which also cancels the request. Racing against a timer only stops you waiting; the request carries on.
05What is the difference between a shallow copy and a deep copy? What does this log?Junior
Primitives are copied by value. Objects, arrays and functions are held by reference, so copying the variable copies the pointer. A shallow copy duplicates the top level and shares everything nested; a deep copy duplicates the whole structure.
Interview line: This is the entire reason React state must be updated immutably. React, memo and dependency arrays compare with Object.is, which for objects compares references. Mutate in place and the reference is unchanged, so as far as React can tell, nothing happened.
- When updating nested React state, do you need a deep copy?
- No, and you should not make one. Copy only the objects along the path you are changing and reuse everything else. That is cheaper, and it keeps the untouched branches reference-equal so memoised components that depend on them do not re-render.
- Does
constmake an object immutable? - No. It prevents reassigning the variable. The object can still be changed.
Object.freezeprevents changes to its own properties, shallowly.
06What is the difference between == and ===? What does this log?Junior
=== compares without converting types. == converts the operands to a common type first, following rules few people can recite. Use === everywhere, with the one accepted exception of value == null, which is true for both null and undefined.
+concatenates if either operand is a string; every other arithmetic operator converts to numbers. Hence"12"and then12.- Arrays convert to strings by joining with commas:
"1,2" + "3". [] == false: the array becomes"", which becomes0, andfalsebecomes0.NaNis not equal to anything, including itself.Number.isNaNandObject.isare how you detect it.0.1 + 0.2is0.30000000000000004. Binary floating point cannot represent those decimals exactly; compare with a tolerance, or work in integers (cents).typeof nullbeing"object"is a bug from JavaScript's first release that can never be fixed. UseArray.isArrayfor arrays.
- Where does truthiness bite in React?
{count && <List />}renders0whencountis zero, because0 && anythingevaluates to0, and React renders numbers. Writecount > 0 &&.||or??for a default value???falls back only onnullandundefined.||falls back on every falsy value, so a legitimate0or empty string gets replaced.
07What is hoisting? What is the difference between var, let and const?Junior
Declarations are processed before any code runs, so a name exists throughout its scope. var and function declarations are usable from the top (a var holds undefined until assigned). let and const exist but cannot be touched until their declaration line; that gap is the temporal dead zone.
- Default to
const; useletwhen the variable is reassigned; do not usevar. constfixes the binding, not the value:const list = []; list.push(1)is fine.- Function declarations are hoisted with their body. Function expressions and arrow functions assigned to a variable follow that variable's rules.
- Classes are hoisted like
let: using one before its declaration throws.
08How does prototypal inheritance work? What does this log?Mid
Every object has a hidden link to another object, its prototype. Reading a property that the object does not have follows that link, and the next, until the property is found or the chain ends. Writing never follows the chain: assignment always creates or updates a property on the object itself.
Assigning settings.theme shadows the inherited property; defaults is untouched. But settings.tags.push(...) does not assign to settings at all. It looks tags up, finds the array on defaults, and mutates that shared array.
classis syntax over this mechanism. Methods live once onDog.prototype; instances hold only their own data.extendslinksDog.prototypetoAnimal.prototype, so a lookup falls through from one to the other.Object.create(proto)makes an object with the given prototype directly, with no constructor involved.
- What is the difference between
__proto__andprototype? prototypeis a property of constructor functions and classes: the object that will become the prototype of instances created withnew.__proto__(properly,Object.getPrototypeOf(obj)) is the actual link an object has to its prototype.- Why is mutable state on a prototype dangerous?
- It is shared by every object that inherits from it. One instance pushing to an inherited array changes it for all of them, as the first example shows.
09Which array methods mutate, and which return a new array? What does this log?Junior
map, filter, slice, concat, flat and the newer toSorted, toReversed, toSpliced and with return a new array. push, pop, shift, unshift, splice, sort and reverse change the array in place. Knowing which is which matters in React, where state must not be mutated.
sort()with no comparator converts elements to strings, so10sorts before2. Always pass(a, b) => a - bfor numbers.sort()sorts in place and returns the same array. Sorting React state directly mutates it; usetoSortedor copy first.mapcalls its callback with(value, index, array).parseInttakes(string, radix), so the index is used as the radix:parseInt("2", 1)isNaN. Usemap(Number)or an explicit arrow.
- Why can you not
breakout offorEach? - It is a function call per item, not a loop. Use
for...ofwhen you needbreak,continueorawait, orsome/findwhen you want to stop at the first match. - How do you remove duplicates from an array?
[...new Set(items)]for primitives. For objects, by key: build aMapkeyed by ID and take its values.
10What causes memory leaks in a React application?Senior
Something long-lived keeps a reference to something that should have been released. In React it is nearly always a subscription, listener or timer created in an effect and never cleaned up, so the callback, and everything its closure captured, outlives the component.
- Find them with the browser's Memory panel: take a heap snapshot, repeat the suspect interaction several times (open and close the dialog, navigate back and forth), snapshot again, and compare. Look for detached DOM nodes and counts that grow with each repetition.
- Strict Mode helps: its mount, unmount, remount cycle makes a missing cleanup visible as duplicated behaviour during development.
- What is the difference between a
WeakMapand aMaphere? - A
Mapkeeps its keys alive. AWeakMapdoes not: when nothing else references the key object, the entry can be collected. That makes it the right structure for attaching data to objects you do not own.
Want reps on this? Drill Async/Await & Promises →
Live coding exercises
The practical round. These are the components and hooks that get asked for again and again. Write each one yourself before opening the solution, out loud, as you would in the room.
01How do you approach a live coding exercise?Junior
Slow down at the start. Clarify what is being asked, say what state you need and where it lives, build the simplest working version end to end, and only then handle loading, errors and edge cases. Narrate throughout: the interviewer is assessing how you think, and silence gives them nothing.
- Clarify (two minutes). What are the inputs and outputs? Which states exist: loading, empty, error? Does it need to persist, be accessible, be styled? Restate the task in one sentence.
- Design out loud. Name the components and the state: what is stored, what is derived, who owns it. Mention the alternative you rejected.
- Happy path first. Get something on screen that works for the main case before polishing any part of it.
- Then the unhappy paths. Loading, error, empty, invalid input, rapid repeated actions.
- Test it by hand and say what you are checking. Fix what you find without apologising.
- Close. Say what you would do with more time: tests, accessibility, extracting a hook, performance for large inputs.
Interview line: If I am unsure about something, I say so and say how I would find out. 'I do not remember whether this fires before paint; I would check the docs, and for now I will assume it does not' is a strong answer. A confident guess that turns out wrong is not.
- What if you get stuck?
- Say where you are stuck and what you have ruled out, then reduce the problem: hard-code the data, drop a requirement temporarily, or write the pure function first and the component second. Asking a precise question is fine; sitting in silence is not.
02Build a counter with increment, decrement and reset, bounded by a minimum and maximum.Junior
One number in state. The buttons use the updater form, clamp the result, and disable themselves at the bounds. The disabled flags are derived during render, not stored.
- Updater function.
setCount((current) => ...)stays correct when the button is clicked faster than React re-renders. initial, notvalue. The name signals that the prop seeds the state once and is not kept in sync.- Derived, not stored.
count <= minis computed each render. A separateisAtMinstate would be a second source of truth. - Icon-only buttons need a name.
aria-labelgives a screen reader something to announce.
- How would you make it controllable by a parent?
- Accept
valueandonChangeand hold no state of its own, like a native input. To support both, use the controlled value whenvalueis provided and internal state otherwise. - How would you add 'hold to repeat'?
- Start an interval on pointer down and clear it on pointer up, pointer leave and unmount. The interval callback must use the updater form, or it will repeat with a stale count.
03Build a todo list: add, toggle, delete, and filter by status.Junior
Two pieces of state: the list of todos and the current filter. The visible list and the remaining count are derived. Every update returns a new array, and each todo carries an ID created once, when it is added.
- Submit on the form. Handling
onSubmitmakes Enter work for free; a click handler on the button does not. - Keys are IDs. With index keys, deleting the first item would shift every checkbox onto the wrong todo.
- The filtered list is not state. It is recomputed from
todosandfilter, so it can never be out of date. - An empty state. A blank area reads as broken; say that there is nothing to show.
- How would you persist the list?
- Initialise state lazily from
localStorageand write back in an effect that depends ontodos, guarding both withtry/catch. Under server rendering, read after mount to avoid a hydration mismatch. - It now has 10,000 todos and typing in the input lags. Why, and what do you do?
- Every keystroke re-renders the whole list. Move the draft input into its own component so its state is local, memoise the row component, and virtualise the list.
- When would you switch to
useReducer? - When the operations multiply (edit, reorder, bulk complete, undo). A reducer keeps every transition in one testable function and gives children a single stable
dispatch.
04Write a useFetch hook.Mid
State is a discriminated union of loading, success and error. An effect keyed on the URL makes the request with an AbortController, and its cleanup aborts, which handles both unmounting and a URL that changes while a request is in flight.
- A union, not three booleans.
isLoading,erroranddataas separate values allow combinations that make no sense. The union lets the compiler narrowstate.dataafter the checks. - The effect is not
async. It declares an async function inside and calls it, so it can still return a cleanup. - Check
res.ok.fetchresolves for a 404 or a 500. - Abort is not an error. An aborted request rejects; that rejection must not be shown to the user.
The trap: useFetch<Repo[]> looks type-safe and is not: the generic is an unchecked assertion about what the server returns. Say so, and mention validating the response for anything that matters.
- What is missing compared with a real data library?
- Caching, de-duplication of identical requests, background revalidation, retries, pagination, mutations with invalidation, and server rendering. That list is the argument for TanStack Query or SWR.
- How would you add a
refetchfunction? - Keep a counter in state, include it in the effect's dependencies, and return a function that increments it.
- The component shows the previous user's repositories for a moment when
userchanges. Why? - It does not here, because the effect sets loading before the request. A version that only sets state on success would keep the old data until the new response arrives, which is sometimes exactly what you want (stale-while-revalidate) and sometimes a bug.
05Build a search box that queries an API as the user types.Mid
The input is controlled and updates on every keystroke. An effect waits for a pause in typing before sending a request, and its cleanup cancels both the pending timer and any in-flight request, so results can never arrive out of order.
- How would you make it a proper autocomplete?
- Keyboard navigation (arrow keys move an active index, Enter selects, Escape closes), the ARIA combobox pattern (
role="combobox",aria-expanded,aria-controls,aria-activedescendanton the input;role="listbox"androle="option"on the results), closing on outside click, and a cache keyed by query so going back a character is instant. - Where would
useDeferredValuefit? - If rendering the results is slow, defer the value passed to the list so typing stays responsive. It does not reduce the number of requests, so it complements the debounce.
06Write a useLocalStorage hook.Mid
It behaves like useState but reads its initial value from localStorage and writes changes back. The details that matter are a lazy initialiser, guarding every storage access with try/catch, not touching window on the server, and listening for changes made in other tabs.
- Lazy initialiser. Reading and parsing storage once, not on every render.
localStoragecan throw. In private browsing, when blocked by policy, or when the quota is full.- It only stores strings.
JSON.stringifyloses dates, maps, sets andundefined. as constmakes the return a tuple, so callers get the same shape asuseState.
The trap: With server rendering this version causes a hydration mismatch: the server renders initialValue, and the client's first render reads the stored value. Either read storage in an effect after mount (accepting a brief flash), or use useSyncExternalStore with a server snapshot.
- How do you stop a theme from flashing on load?
- No hook can run early enough. Put a tiny blocking script in the document head that reads the preference and sets a class or attribute on
<html>before the first paint, or store the preference in a cookie so the server can render the right theme. - Why is this unsuitable for auth tokens?
- Anything in
localStorageis readable by every script on the page, so one XSS bug exposes it. Session credentials belong inHttpOnlycookies.
07Build a countdown timer. Write the useInterval hook it needs.Mid
The difficulty is that an interval outlives renders, so its callback goes stale. useInterval keeps the latest callback in a ref and reads it from the timer, so the interval is created once and restarted only when the delay changes. Passing null as the delay pauses it.
- Why not put the callback in the dependency array? It is a new function every render, so the interval would be torn down and recreated each second, drifting further each time.
- Pausing by delay.
nullskips the effect, and the cleanup of the previous run has already cleared the timer. tickingis derived fromrunningandremaining. When the count reaches zero the interval stops with no extra state.- In React 19.2,
useEffectEventreplaces the ref: wrap the callback in it and call that from the interval.
The trap: setInterval is not a clock. Timers are delayed when the tab is in the background or the main thread is busy, so counting ticks drifts. For an accurate timer, store the end timestamp and compute remaining from Date.now() on each tick.
- What if
secondschanges while it is running? - As written, nothing: the prop only seeds the state. To restart for a new duration, render it with
key={seconds}so React creates a fresh instance.
08Build an accessible accordion.Mid
State is the set of open panel IDs. Each header is a real <button> inside a heading, with aria-expanded and aria-controls; each panel is a region labelled by its header. A prop switches between one-open-at-a-time and many.
- A button, not a div. It is focusable and responds to Enter and Space with no extra code.
aria-expandedtells assistive technology whether the section is open;aria-controlssays which element it governs.hiddenremoves the panel from both the layout and the accessibility tree while keeping it mounted, so state inside it survives.useIdkeeps the IDs unique when several accordions are on the page.
- Should closed panels be unmounted?
- It depends. Unmounting (
{open && ...}) saves work for heavy content but loses the panel's internal state and scroll position. Keeping it mounted and hidden preserves both. Say which you chose and why. - How would you animate the height?
- Animating to
height: autois the classic problem. Options are a CSS grid row transitioning from0frto1fr, the newerinterpolate-size, or measuring the content. And honourprefers-reduced-motion. - Is there a native element for this?
<details>and<summary>give you a disclosure with no JavaScript, and thenameattribute groups them so only one is open. Use it when you do not need custom behaviour.
09Build a star rating component.Mid
A controlled component: value in, onChange out. Under the stars it is a radio group, which gives keyboard support and screen reader semantics for free. The only local state is which star the pointer is over, for the hover preview.
- Controlled by the parent, like a native input, so it drops into any form and can be reset from outside.
- Radio inputs do the hard part. Arrow keys move between stars, Tab enters and leaves the group, and a screen reader announces '3 of 5, radio button, selected'.
- The glyphs are decoration (
aria-hidden); the accessible name comes from the hidden text. hovered ?? value, not||: a rating of 0 is a real value.
- How would you allow clearing the rating?
- Radio groups cannot be unselected by clicking, so add an explicit 'Clear' button, or treat a click on the current value as a reset (and say that it is not discoverable by keyboard without the button).
- And half stars?
- Double the number of radio options (0.5 steps) and draw each star from two halves, or switch the underlying control to a range input, which is the right semantic for a continuous value.
10Build a sign-up form with validation, without a library.Mid
Values and 'touched' flags are state; errors are derived by a pure validate function on every render. Errors are shown for a field once it has been blurred, and for all fields after a submit attempt. The submit handler prevents the default, validates, and tracks a submitting status.
- Errors are not state. Storing them would mean remembering to update them on every change. Deriving them cannot go stale.
touchedstops the form from complaining about fields the user has not reached yet.noValidateturns off the browser's own validation bubbles so yours are the only ones.- The button stays enabled when the form is invalid. A disabled submit gives no explanation; clicking it and seeing the errors does.
- One
handleChangefor all fields, keyed by the input'sname.
- How would you add 'confirm password' or an async 'email already taken' check?
- Cross-field rules belong in
validate, which sees all values. An async check runs on blur or debounced on change, stores its result separately, and is merged into the displayed errors; the server must still enforce uniqueness on submit. - At what point do you bring in a library?
- When there are many fields, dynamic field arrays, nested objects or performance problems from re-rendering on every keystroke. React Hook Form with a schema removes most of this code.
11Build a table with search, sorting and pagination.Mid
Three small pieces of state: the query, the sort, and the page. Everything else is a pipeline computed from them: filter, then sort, then slice. Sorting copies the array first, and the page is clamped during render instead of being reset in an effect.
- The generic ties
columnstorows: a column key that is not a property of the row type does not compile. [...filtered].sort(...):sortmutates, andfilteredmay be therowsprop itself.- Resetting the page happens in the event handler that changes the query, not in an effect watching the query.
aria-sorton the header and a real<button>inside it make the sorting usable with a keyboard and a screen reader.
- The dataset has two million rows. What changes?
- Everything moves to the server: the query, sort and page become request parameters (and belong in the URL), the component renders one page of results, and the client's job becomes debouncing input, showing loading state and keeping previous data visible while the next page loads.
- Why keep table state in the URL?
- So a filtered, sorted view can be bookmarked and shared, survives a refresh, and works with the back button.
12Implement infinite scroll.Senior
Render the items followed by an empty sentinel element, and watch the sentinel with an IntersectionObserver. When it comes into view, load the next page using the cursor from the previous response, and append. Guard against loading twice, and stop when the server says there is no more.
The trap: Infinite scroll has real costs: the footer becomes unreachable, there is no way to link to a position, Back loses your place unless you restore it, and the DOM grows without bound. For long sessions combine it with virtualisation, and consider a 'Load more' button, which keeps the user in control.
- How do you restore the scroll position when the user comes back?
- Keep the loaded pages in a cache that outlives the component (a query library does this), and let the browser or router restore the scroll offset once the same content has rendered.
- How do screen reader users know more content arrived?
- Announce it in a live region ('20 more results loaded') and make sure focus is not lost. A list that silently grows is invisible to them.
13Code review: find the bugs in this component.Mid
There are seven: an async effect, a missing dependency, no cancellation, state mutation, an && that renders 0, index keys, and a handler that is called during render instead of passed.
The fixed version also moves the click from the <li> to a <button>, so the row can be selected from the keyboard, and stops a click on Remove from also selecting the row through bubbling.
- What is still missing from the fixed version?
- Loading and error states, a check of
res.ok, and validation of the response shape. Andselectedcan point at a user that has since been removed, which should be handled by clearing it or deriving the selected user from the list.
14Spot the bug: three small TypeScript functions that compile and are wrong.Junior
A filter that keeps the odd numbers, a loop that is off by one at both ends, and a factorial that always returns zero. All three type-check, which is the point: the compiler verifies types, not logic.
- With
noUncheckedIndexedAccessenabled, the buggysumwould not have compiled:numbers[i]would benumber | undefined. - Iterating with
for...oforreduceremoves the index, and with it the whole class of off-by-one errors. - Other favourites in this style: a class method that reads a field without
this., a property assigned in a constructor but never declared, class members separated by commas, and a function annotated to returnany[]when it clearly returnsnumber[].
- The snippet you are handed has no bug. What do you say?
- That it is correct, and why. Some interviewers include a clean snippet on purpose; inventing a problem to have something to say is worse than confidently finding none.
15Implement a type-safe event emitter in TypeScript.Senior
Make the class generic over a map from event name to payload type. on and emit are then generic over a key of that map, so the event name is checked, and the listener's parameter and the emitted payload are inferred from it.
K extends keyof Eventscaptures the specific event name, andEvents[K]looks up its payload. That pairing is the whole trick.- Returning the unsubscribe function from
onfits React's effect cleanup exactly:useEffect(() => emitter.on("login", handler), []). - One honest assertion. The map stores listeners for different events together, so the lookup in
emitneeds a cast. The public API is fully checked; the unsafety is confined to one private line.
The trap: The event map is declared with type, not interface. An interface is not assignable to Record<string, unknown> because it could be extended by declaration merging, so TypedEmitter<SomeInterface> fails the constraint.
- How would you make the payload optional for events that carry none?
- Type the rest of the arguments as a conditional tuple:
...args: Events[K] extends undefined ? [] : [payload: Events[K]]. Thenemit("logout")andemit("login", { ... })are both checked. - What happens if a listener unsubscribes while an event is being emitted?
- A
Settolerates deletion during iteration, and a listener removed before its turn is skipped. If you need 'everyone registered at the start gets called', iterate over a copy.
16Design a parking lot using object-oriented principles in TypeScript.Mid
Model the two things that vary, spot kinds and vehicle kinds, as unions, and put the rules for which vehicle fits where in one data table. The lot tracks free spots per kind and hands out tickets; every query the brief asks for becomes a one-line getter.
A typical brief: the lot has motorcycle, car and large spots. A motorcycle can park anywhere, a car in a car or large spot, and a van in one large spot or three car spots. Report total and remaining spots, whether the lot is full or empty, whether a given kind of spot is full, and how many spots vans occupy. Ask about anything unstated, such as whether the van's three spots must be adjacent.
- Rules as data. Adding a bus, or letting motorcycles share a spot, is a new row in
PARKING_OPTIONS, not a new branch inpark. - Unions over classes. A
Vehiclebase class withMotorcycle,CarandVansubclasses would add three files and no behaviour. Reach for classes when the variants carry different logic. - Encapsulation.
#freeand#ticketsare private, so the counts cannot be put out of sync from outside; the only way to change them isparkandleave. - The ticket is the source of truth for what must be given back, so
leavecannot free the wrong number of spots.
- The van's three car spots must be next to each other. What changes?
- Counts are no longer enough. Model each row as an array of spots and search for a run of three free ones, storing the indices on the ticket. Mention the cost: allocation becomes a scan, and fragmentation becomes possible.
- How would you extend this to multiple levels, or to pricing?
- Compose: a
Garageholding severalParkingLotlevels and trying each in turn. Pricing is a separate object that takes a ticket and a duration, so the lot does not need to know about money. - What would you test?
- Each vehicle's preference order, the fallback when the preferred spot is full,
leaverestoring the exact count, an unknown ticket ID, and the full and empty boundaries.
Want reps on this? Drill Hooks & Lifecycles →