React Developer Interview Questions
Core Overview
Master frontend engineering questions on JavaScript ESNext, React hooks, rendering, state management, and web performance.
Ready to test your knowledge?
Launch a focused practice session to review questions without distraction.
What is a closure in JavaScript, and how can closures cause memory leaks in React applications?
Direct Answer
A closure is a function that retains access to its lexical scope even when executed outside that scope. Memory leaks occur when closures capture large outer variables inside stale callbacks.
Detailed Explanation
In JavaScript, a closure is created every time a function is defined inside another function. The inner function maintains a reference to the outer function's variable bindings (its lexical environment) even after the outer function has returned.
React Memory Leaks: In React, hooks like useEffect or useCallback create closures over the component's props and state. If you register a closure inside an asynchronous operation (like setInterval or an active WebSocket stream listener) but fail to clean it up when the component unmounts, the closure remains active in memory. Since it retains references to the component's local scope variables, the entire component tree and its state cannot be garbage collected, causing memory leaks.
Code Example
// Stale closure and memory leak example:
useEffect(() => {
const handleScroll = () => {
console.log("Current state:", count); // Closure captures 'count'
};
window.addEventListener("scroll", handleScroll);
// Missing cleanup! window retains handleScroll, leaking count
}, []);
Common Interview Pitfalls
- Forgetting to return a cleanup function in `useEffect` that removes event listeners or cancels timeouts.
- Assuming that setting local variables to `null` inside a closure prevents garbage retention of the parent context.
Explain the JavaScript Event Loop and the execution priority difference between microtasks and macrotasks.
Direct Answer
The Event Loop coordinates code execution. Microtasks (Promises) run immediately after the current task finishes, before macrotasks (setTimeout, UI render) run.
Detailed Explanation
JavaScript is single-threaded and uses the Event Loop to handle asynchronous operations. The execution queue is split into two priority structures:
1. Call Stack: Executes synchronous operations.
2. Microtask Queue: High priority. Includes Promise.then callbacks, MutationObserver, and queueMicrotask. The Event Loop processes the *entire* microtask queue immediately after the current synchronous script finishes and before rendering or executing other tasks.
3. Macrotask (Task) Queue: Low priority. Includes setTimeout, setInterval, setImmediate, and UI layout/rendering events. The Event Loop processes exactly *one* macrotask per loop cycle, followed by clearing any new microtasks that were enqueued, and optionally running a browser paint step.
Code Example
console.log("1. Sync");
setTimeout(() => console.log("4. Macrotask"), 0);
Promise.resolve().then(() => console.log("3. Microtask"));
console.log("2. Sync");
// Outputs: 1. Sync -> 2. Sync -> 3. Microtask -> 4. Macrotask
Common Interview Pitfalls
- Expecting `setTimeout(fn, 0)` to execute before a Promise microtask resolve callback because it was declared earlier in code.
- Blocking the call stack with heavy computation, which stalls both microtask execution and browser UI rendering.
How does prototypal inheritance work in JavaScript, and how does it compare to ES6 class syntax?
Direct Answer
Prototypal inheritance links objects directly using an internal prototype reference. ES6 classes are syntactic sugar over this prototype chain.
Detailed Explanation
Every object in JavaScript has an internal link to another object called its prototype (accessible via Object.getPrototypeOf(obj) or __proto__). When accessing a property on an object, JavaScript checks the object itself; if missing, it searches up the prototype chain until it finds the property or reaches null.
ES6 Classes: Introduced as syntactic sugar over prototypes. Under the hood, declaring class User still creates a constructor function and assigns methods to User.prototype. It does not introduce a classical class-based object model (like Java) but makes prototype syntax cleaner and easier to read.
Code Example
function User(name) { this.name = name; }
User.prototype.sayHi = function() { return "Hi " + this.name; };
const john = new User("John");
console.log(john.sayHi()); // Resolves via prototype chain
Common Interview Pitfalls
- Assuming ES6 classes introduce true static type schemas at runtime in compiled JS files.
- Modifying shared prototype properties directly on native objects (`Object.prototype`) which pollutes the global scope.
Describe key TypeScript utility types and how mapped types support component props modification.
Direct Answer
Utility types like Pick, Omit, and Partial modify existing interfaces. Mapped types iterate over keys to create new type definitions dynamically.
Detailed Explanation
TypeScript provides built-in utility types to transform existing structures:
T optional.K from T.K from T.Mapped Types: Allow creating new types by iterating over keys of an existing type. They use the in keyword, acting as a map function over union types. E.g., making all properties read-only or prefixing props names. This is widely used in React for component wrappers that modify prop lists dynamically.
Code Example
type User = { id: string; name: string; email: string };
// Mapped Type: makes all properties read-only
type ReadonlyUser = { readonly [P in keyof User]: User[P] };
// Utility usage:
type UserSummary = Omit<User, "email">; // { id: string; name: string }
Common Interview Pitfalls
- Using `any` instead of Generics (`<T>`) for flexible utility functions, disabling compile-time type checking.
- Over-nesting utility types, making build error messages unreadable.
How do type guards and discriminated unions implement safe type narrowing in TypeScript?
Direct Answer
Type narrowing refines loose types into specific types. Discriminated unions use a common string literal field to determine the active type.
Detailed Explanation
TypeScript type narrowing refines a broad type (like string | number or a generic object union) into a specific type within conditional blocks:
typeof, instanceof, or custom type predicates (parameterName is Type) to verify type properties. Inside the guarded block, TS narrows the type automatically.type: "success" | "error"). By checking this single property in a switch or if statement, TypeScript narrows the type of the remaining object properties safely.Code Example
type NetworkState =
| { type: "loading" }
| { type: "success"; data: string }
| { type: "error"; message: string };
function render(state: NetworkState) {
if (state.type === "success") {
console.log(state.data); // Narrowed: data is safe to access
}
}
Common Interview Pitfalls
- Using custom type predicates that return true but fail to guarantee type safety in runtime objects.
- Forgetting to add a default case checking for `never` type to ensure exhaustiveness in switch statements.
What is the difference between ES6 modules and CommonJS imports?
Direct Answer
CommonJS (`require`) imports modules synchronously at runtime. ES6 modules (`import`) are loaded statically at compile time, enabling tree shaking.
Detailed Explanation
Node.js and modern browsers handle JavaScript modularity using different systems:
const module = require("./module") and module.exports = ....if blocks. Evaluated at runtime.import { name } from "./module" and export const name = ....Code Example
// CommonJS (Dynamic)
if (condition) {
const service = require("./service");
}
// ESM (Static, compile-time verified)
import { service } from "./service";
Common Interview Pitfalls
- Attempting to use CJS variables like `__dirname` or `__filename` directly in ESM files without importing utility equivalents.
- Mixing import and require syntaxes within a single project without proper transpiler configuration.
How do the Virtual DOM and React Fiber reconciliation work?
Direct Answer
The Virtual DOM represents UI states in memory. React Fiber is a rewrite of the reconciler that breaks work into chunks, allowing prioritization and async rendering.
Detailed Explanation
React decouples rendering logic from DOM updates:
1. Virtual DOM: An in-memory lightweight representation of the real DOM tree. When state changes, React builds a new Virtual DOM tree.
2. Reconciliation: React compares the new tree with the old tree (using a diffing algorithm) to find the minimum set of changes to apply to the real DOM.
3. React Fiber: React's current reconciliation engine. The old reconciler worked recursively and synchronously, blocking the main thread during large updates. Fiber breaks reconciliation into small units of work called "fibers". It can pause, resume, discard, or reuse rendering work dynamically based on priority (e.g. prioritizing user keystrokes over background data fetches).
Code Example
// React Fiber schedules this component update as a low-priority task
// if wrapped inside useTransition, keeping the UI interactive.
const [isPending, startTransition] = useTransition();
startTransition(() => {
setHeavyListState(data);
});
Common Interview Pitfalls
- Assuming Virtual DOM makes React faster than vanilla JS (vanilla DOM manipulation is always faster; VDOM simply provides a declarative abstraction that minimizes slow, batch DOM writes).
- Modifying the real DOM directly using vanilla JS selectors while React is managing the same nodes, causing state mismatches.
What is Concurrent Rendering in React 18, and how does Server Hydration work?
Direct Answer
Concurrent rendering allows React to pause rendering to keep the browser responsive. Hydration is the process of attaching event listeners to static server-rendered HTML.
Detailed Explanation
React 18 introduced architectural features for performance:
Code Example
// Suspense enables streaming hydration, hydrating components
// as they load, instead of waiting for the entire bundle.
<Suspense fallback={<Spinner />}>
<HeavyWidget />
</Suspense>
Common Interview Pitfalls
- Accessing `window` or `document` variables during initial render in SSR components, causing hydration mismatches (wrap in `useEffect` instead).
- Using random generators (like Math.random()) in component rendering, causing client and server HTML output mismatch.
How do class component lifecycle methods map to React functional Hooks?
Direct Answer
`componentDidMount` maps to `useEffect` with an empty array. `componentDidUpdate` maps to `useEffect` with dependency items. `componentWillUnmount` maps to the return cleanup function.
Detailed Explanation
Class component lifecycle methods are consolidated into the useEffect hook in functional components:
useEffect with an empty dependency array ([]).useEffect with target dependency values listed inside the array ([dependency1]).useEffect.React.memo for custom rendering control.Code Example
// Functional component equivalent:
useEffect(() => {
console.log("Mounted (componentDidMount)");
return () => {
console.log("Unmounting (componentWillUnmount)");
};
}, []);
Common Interview Pitfalls
- Leaving the dependency array empty but referencing local variables inside `useEffect` (creates stale closures that never read updated state).
- Triggering state updates inside a `useEffect` without dependencies, causing infinite re-render loops.
Why are keys required in React lists, and what are the risks of using array indexes as keys?
Direct Answer
Keys give list items a stable identity, helping React identify which items changed, were added, or were removed. Using indices as keys causes render bugs and state leaks.
Detailed Explanation
During reconciliation, React diffs children to optimize DOM updates. If a list has no keys, React updates elements positionally. E.g., if you prepend an item, React assumes every item shifted index positions, re-rendering the entire list.
Risks of Indices: If you use array indices (key={index}) and the list is filtered, sorted, or items are inserted:
1. Rendering Bugs: Inputs or checkboxes (which maintain internal DOM state) will remain at their index position, showing outdated values on newly shifted elements.
2. Performance Lag: React is forced to re-render all items instead of simply prepending the new DOM node.
Code Example
// Correct list implementation:
<ul>
{items.map((item) => (
<li key={item.uuid}>{item.text}</li> // Stable unique key
))}
</ul>
Common Interview Pitfalls
- Using `Math.random()` or temporary timestamps as keys (generates new keys on every render, forcing complete DOM nodes deletion and recreation).
- Forgetting keys on wrapper tags when rendering lists of multiple elements.
How do React Portals work, and how does event bubbling behave across portal boundaries?
Direct Answer
Portals render children into a different DOM node outside the parent hierarchy. However, events still bubble up through the virtual React tree.
Detailed Explanation
React Portals (ReactDOM.createPortal(child, container)) allow rendering a component into a different DOM subtree (e.g. appending a Modal to the end of document.body to avoid overflow or z-index cropping issues).
Event Bubbling: Even though the portal child exists in a different physical DOM location, it still behaves as a standard child in the React Virtual Tree. Events (like clicks) triggered inside the portal bubble up to React parent wrappers. This means a parent container can capture clicks originating from a portal Modal even if they are in completely separate subtrees in the real DOM.
Code Example
import { createPortal } from "react-dom";
function Modal({ children }) {
// Renders under #modal-root DOM node
return createPortal(
<div className="modal">{children}</div>,
document.getElementById("modal-root")
);
}
Common Interview Pitfalls
- Assuming that because a portal is appended to `document.body`, event handlers on parent React components will not trigger (leads to unintended triggers if event propagation is not stopped).
- Failing to verify the existence of the container DOM node during SSR before calling `createPortal`.
Compare Server-Side Rendering (SSR) and Static Site Generation (SSG) in Next.js.
Direct Answer
SSR generates HTML dynamically for every incoming request. SSG pre-builds static HTML pages at build time, offering faster loading speeds.
Detailed Explanation
Next.js supports different rendering strategies for web applications:
getServerSideProps or dynamic fetch options).Code Example
// Next.js App Router default Server Component:
// Fetching with force-cache defaults to SSG behavior
async function Page() {
const res = await fetch("https://api.example.com/data", { cache: "force-cache" });
const data = await res.json();
return <div>{data.title}</div>;
}
Common Interview Pitfalls
- Using SSR for static marketing pages, wasting server processing resources on request-time renders.
- Assuming client-side cookies or search params can be read statically during SSG build phases.
How does state batching work in React, and when should you use functional state updates?
Direct Answer
React batches state updates within event handlers into a single render pass. Use functional updates (`setCount(c => c + 1)`) when updates depend on previous state.
Detailed Explanation
React optimizes rendering through Automatic Batching. When you trigger multiple state updates in a single event handler (even inside promises, timeouts, or microtasks), React batches them into a single update queue, running only one re-render pass.
Functional Updates: Because state updates are queued rather than applied immediately, reading local state values sequentially inside a handler will reference the old render state, leading to outdated calculations. To resolve this, pass a callback function to the state setter (setState(prev => prev + 1)). React passes the latest queued state to the callback, ensuring updates apply sequentially.
Code Example
// State Batching Issue:
const handleAdd = () => {
setCount(count + 1);
setCount(count + 1); // Both reference same initial 'count'
}; // Result is count + 1
// Fix:
const handleAddCorrect = () => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}; // Result is count + 2
Common Interview Pitfalls
- Calling state setters and immediately reading the state variable expecting it to show the updated value on the next line of code.
- Over-relying on `useEffect` to synchronize values that could be derived directly from existing state variables.
Explain dependency array rules in `useEffect` and how cleanup functions execute.
Direct Answer
The dependency array dictates when an effect re-runs. The cleanup function runs before the effect re-runs and during component unmount, preventing leaks.
Detailed Explanation
The dependency array tells React when to execute the effect block:
Cleanup Execution: If the effect returns a function, React executes this cleanup function:
1. Before running the main effect block again (to clean up residual event listeners/subscriptions from the previous run).
2. When the component unmounts. This is critical to release web socket connections, intervals, or DOM event listeners.
Code Example
useEffect(() => {
const socket = connect(url);
return () => {
socket.disconnect(); // Clean up on key change or unmount
};
}, [url]);
Common Interview Pitfalls
- Omitting objects or functions from the dependency array while referencing them inside the effect, causing stale closure bugs.
- Declaring objects directly inside dependencies without memoization, triggering infinite re-run loops.
Compare `useMemo` and `useCallback` and when to apply them for performance.
Direct Answer
`useMemo` caches the calculated *result* of a function. `useCallback` caches the *function definition* itself to prevent reference changes.
Detailed Explanation
Both hooks cache references to prevent redundant calculations and re-renders, but they serve different targets:
useCallback preserves the reference, allowing child memoization components (React.memo) to skip re-renders.Code Example
// Caches calculation result
const sortedList = useMemo(() => expensiveSort(list), [list]);
// Caches function reference
const handleClick = useCallback(() => console.log("Clicked"), []);
Common Interview Pitfalls
- Wrapping every single function and variable in `useCallback`/`useMemo` without profiling (adds overhead; simple calculations are cheaper than hook dependency arrays comparison).
- Forgetting that `useCallback` dependencies must include all values referenced inside the function to prevent stale closure bugs.
What is the purpose of `useRef`, and how does it differ from state regarding rendering?
Direct Answer
`useRef` persists mutable values across renders without triggering a re-render when the value changes. It is also used to reference DOM elements directly.
Detailed Explanation
useRef returns a mutable object with a .current property. It serves two main purposes:
1. State Persistence without Render: Modifying ref.current does *not* trigger a component re-render. It is ideal for storing metadata that has no direct UI representation (like timer IDs, scroll positions, or state references).
2. Direct DOM Access: Passing a ref to a JSX element binds the native DOM node to ref.current after mounting, allowing direct focus control, measurements, or animation triggering.
Code Example
const timerRef = useRef(null);
const startTimer = () => {
timerRef.current = setInterval(() => console.log("Tick"), 1000);
};
const stopTimer = () => {
clearInterval(timerRef.current); // Accesses persisted timer ID
};
Common Interview Pitfalls
- Reading or writing `ref.current` during the render phase (this can lead to inconsistent UI states; only interact with refs inside event handlers or `useEffect`).
- Expecting ref updates to instantly update JSX variables that depend on them (requires React state instead).
How do you design custom Hooks in React, and how does state isolation work?
Direct Answer
Custom hooks are functions that call other React hooks. They encapsulate stateful logic, but every component that calls them gets completely isolated state.
Detailed Explanation
Custom hooks allow extracting component logic into reusable functions. A custom hook must start with the prefix use (enabling ESLint hooks rules) and can call native React hooks internally.
State Isolation: Sharing a custom hook does *not* share state. If ComponentA and ComponentB call useToggle(), they execute independent hook registration tracks in React. Each caller gets a completely isolated copy of state variables, allowing modularity without cross-component coupling.
Code Example
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue((v) => !v);
return [value, toggle];
}
// Usage: const [isOpen, toggleOpen] = useToggle();
Common Interview Pitfalls
- Failing to prefix custom hooks with `use`, which bypasses static analysis checks for hook execution safety rules.
- Assuming that custom hooks act as global state-sharing nodes across different component render contexts.
What are the core Rules of Hooks, and why must they be executed at the top level?
Direct Answer
Hooks must only be called at the top level of a component (never inside conditions/loops) and only from React functions, ensuring stable hook index allocations.
Detailed Explanation
React has two strict Rules of Hooks:
1. Only Call Hooks at the Top Level: Do not call hooks inside loops, conditional blocks, or nested functions.
2. Only Call Hooks from React Functions: Call them from functional components or custom hooks, never from plain JS helper methods.
The Rationale: Under the hood, React does not track hook states by name (there are no string keys). It tracks them by execution order index (an array of states). If you wrap a hook in an if condition and it is skipped in a subsequent render, all downstream hook indices shift, causing React to associate state updates with completely incorrect variables, leading to silent rendering bugs.
Code Example
// CRITICAL RULE ERROR:
if (condition) {
useEffect(() => { ... }, []); // Can shift hook index!
}
// Correct implementation:
useEffect(() => {
if (condition) { // Place conditions INSIDE the effect block instead
// Run logic
}
}, [condition]);
Common Interview Pitfalls
- Calling hooks inside helper functions declared outside the React component structure.
- Adding conditional returns above hook calls, which dynamically changes the total number of executed hooks.
Compare React Context API with Redux for application-wide state management.
Direct Answer
Context is a dependency injection tool that passes values down without prop drilling, causing all consumers to re-render. Redux is a state-management engine with selector-based re-renders.
Detailed Explanation
Choosing between Context and Redux depends on update frequency and state complexity:
useSelector). Components only re-render if the specific, extracted property changes (doing deep comparison checks), preventing unnecessary tree renders.Code Example
// Selector optimization: Only re-renders if user.role changes
const userRole = useSelector(state => state.auth.user.role);
Common Interview Pitfalls
- Using a single, massive Context for the entire application's state, causing complete application re-renders on minor input changes.
- Writing stateful mutation logic directly inside Context providers instead of keeping updates transactional.
What is prop drilling, and how can you resolve it in React applications?
Direct Answer
Prop drilling is passing data through multiple intermediate components that do not need it. Resolve using component composition or the Context API.
Detailed Explanation
Prop drilling occurs when a parent state is passed down through five layers of child components solely to reach a deeply nested consumer, forcing intermediate components to declare and forward props they do not use, creating tight coupling.
Resolutions:
1. Component Composition: Pass the child component directly as a prop (children or slots). This lets the parent inject state directly into the consumer without intermediate components knowing about it.
2. Context API: Wrap the parent in a Context Provider, allowing the deep child to consume the state directly via useContext.
Code Example
// Composition solution:
function App() {
const [user] = useState({ name: "Alice" });
return <PageLayout userProfile={<UserProfile user={user} />} />;
} // PageLayout doesn't touch or forward the 'user' prop
Common Interview Pitfalls
- Instantly reaching for global state managers (like Redux) to solve simple prop forwarding that could be solved via composition.
- Passing entire state setters down the tree instead of event callbacks.
How does Redux Toolkit simplify Redux boilerplate, and what are slices and thunks?
Direct Answer
Redux Toolkit eliminates Redux boilerplate by combining actions and reducers in slices, using Immer for immutable updates, and integrating thunks by default.
Detailed Explanation
Traditional Redux required separate files for actions, reducers, and constants. Redux Toolkit (RTK) simplifies this:
createSlice. It automatically generates actions and action creators from a single reducer configuration. It uses Immer internally, allowing you to write mutable state mutations (e.g. state.count = 5) which compile to safe, immutable copy updates.createAsyncThunk out-of-the-box. It manages async states automatically, dispatching lifecycle actions (pending, fulfilled, rejected) that slices handle in extraReducers.Code Example
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; } // Safe mutable-like code via Immer
}
});
Common Interview Pitfalls
- Mutating state outside of RTK slices (Immer is only active inside createSlice reducers; raw mutations elsewhere cause silent state update bugs).
- Declaring state fields that hold non-serializable objects (like class instances, maps, or functions) in store configurations.
Compare Zustand and Recoil for state management architectures.
Direct Answer
Zustand is a simple, flux-based external store using hooks with minimal boilerplate. Recoil is an atomic state manager that binds to the React virtual tree.
Detailed Explanation
Comparing state management paradigms:
Code Example
// Zustand Store Example:
import { create } from "zustand";
const useStore = create((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 }))
}));
Common Interview Pitfalls
- Calling Zustand state getters inside components without passing a selector function, causing the component to re-render on *any* store state change.
- Failing to isolate concerns by creating a single global Recoil root for disconnected features.
How do you synchronize React state with URL parameters in Next.js applications?
Direct Answer
Read URL values using `useSearchParams`. Update URL state using `useRouter` and `usePathname` by passing search parameters to router push methods.
Detailed Explanation
Using the URL as a single source of truth is a best practice for page filters, sorting, and pagination, making pages shareable:
1. Read URL State: Use the useSearchParams hook to parse current query parameters inside client components.
2. Update URL State: Construct a new URLSearchParams object from existing parameters. Add or remove keys, and apply updates using router.push or router.replace via useRouter.
3. Transition Control: Set scroll: false during transitions to prevent Next.js from resetting scroll position on minor query parameter changes.
Code Example
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const updateFilter = (value) => {
const params = new URLSearchParams(searchParams);
params.set("filter", value);
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
};
Common Interview Pitfalls
- Synchronizing state variables manually in both local `useState` AND the URL, causing race conditions and out-of-sync UI updates (use the URL as the *only* source instead).
- Forgetting to wrap search parameter reader components inside a `<Suspense>` block, causing hydration failures during build compilation.
What is Optimistic UI, and how do you implement it in React?
Direct Answer
Optimistic UI updates the interface instantly with the expected success state before the server API responds, rolling back if the request fails.
Detailed Explanation
Optimistic UI creates a responsive user experience by assuming server requests will succeed. Instead of showing spinners while an API resolves (like bookmarking an item), the UI updates instantly:
1. User Action: The client updates local state with the expected response immediately.
2. API Call: Trigger the actual background network request.
3. Rollback handling: Save a copy of the previous state before updating. If the API request succeeds, keep the new state. If the API fails, restore the saved backup state and show an error toast.
Code Example
// React 19 provides useOptimistic specifically for this:
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(state, newLikeCount) => newLikeCount
);
Common Interview Pitfalls
- Failing to handle API failure cases, leaving the user with an out-of-sync UI state that doesn't match the server database.
- Using optimistic updates for high-security transactions (like checkouts) where server confirmation is mandatory before progression.
How does `React.memo` optimize rendering, and what are custom comparison functions?
Direct Answer
`React.memo` is a higher-order component that skips re-rendering a component if its props are unchanged (using shallow comparison).
Detailed Explanation
By default, when a parent component renders, all of its child components render recursively. To optimize this, wrap a child component with React.memo:
===) on every prop. If props are identical, React reuses the last rendered output instead of executing the component function.React.memo(Component, (prevProps, nextProps) => boolean). Return true if props are equal (skips render), or false to force a render.Code Example
const ProductCard = React.memo(function Card({ product }) {
return <div>{product.name}</div>;
}, (prev, next) => prev.product.id === next.product.id);
Common Interview Pitfalls
- Wrapping components that receive children props in `React.memo` (inline children are recreated as new objects on every render, making props comparison fail).
- Using memoization on cheap-to-render components, adding memory comparison overhead without saving CPU cycles.
What is list virtualization, and how does it optimize rendering for large datasets?
Direct Answer
List virtualization renders only the DOM nodes currently visible in the browser viewport, replacing off-screen items with simulated padding.
Detailed Explanation
If you attempt to render a list containing 10,000 items directly, the browser has to create and keep 10,000 DOM nodes in memory, causing high memory usage, input lag, and long paint times.
List Virtualization: Renders only the items currently within the user's viewport (plus a small buffer window). As the user scrolls:
1. Items moving out of view are unmounted from the DOM.
2. New items entering view are mounted.
3. The container uses absolute positioning or height padding spacers to simulate the entire list scroll height, keeping scrollbars functional while only keeping 20-30 active DOM nodes in memory.
Code Example
// Using a virtualized list container pattern
// Only visible items are rendered within the container height
<VirtualList
height={500}
itemCount={10000}
itemSize={50}
renderItem={({ index, style }) => <Row index={index} style={style} />}
/>
Common Interview Pitfalls
- Rendering large table lists without virtualization, causing browser layout freezes on search filter inputs.
- Configuring dynamic row heights in virtual lists without caching previous row dimensions, causing scrollbar flickering.
Explain code splitting using dynamic imports and Suspense in React.
Direct Answer
Code splitting splits JS bundles into smaller chunks. `React.lazy` imports components dynamically, loading them on demand inside Suspense boundaries.
Detailed Explanation
By default, bundlers pack all imported code into a single, massive JavaScript file. The client must download the entire bundle before the page becomes interactive, causing poor Largest Contentful Paint (LCP) speeds.
Code Splitting: Splits code into multiple bundle chunks. Using dynamic imports (const MyComponent = React.lazy(() => import("./MyComponent"))), React loads the target component JS file *only* when it is rendered. Wrap lazy components in a <Suspense fallback={<Loader />}> boundary to display loading states while the bundle chunk is fetched in the background.
Code Example
import React, { Suspense, lazy } from "react";
const HeavyChart = lazy(() => import("./HeavyChart"));
function Dashboard() {
return (
<Suspense fallback={<div>Loading Chart...</div>}>
<HeavyChart />
</Suspense>
);
}
Common Interview Pitfalls
- Declaring `React.lazy` imports inside a component render loop (causes the component chunk to be re-downloaded and re-initialized on every render).
- Forgetting to define a fallback prop on the `<Suspense>` component, throwing a compilation validation error.
Why should you prefer `@testing-library/user-event` over `fireEvent` in React tests?
Direct Answer
`user-event` simulates full user interactions (e.g., typing triggering focus/keydown events) rather than firing raw, isolated DOM events via `fireEvent`.
Detailed Explanation
Testing should resemble how users interact with the page. React Testing Library provides two event simulation tools:
fireEvent.change(input) simply fires a change event without focusing the input, firing keydown events, or updating cursor positions.userEvent.type(input, "hello") will focus the element, trigger hover states, fire keydown, keypress, and keyup events for every character, and update selection scopes, matching actual browser and user inputs.Code Example
import userEvent from "@testing-library/user-event";
test("submits form", async () => {
render(<LoginForm />);
const user = userEvent.setup();
await user.type(screen.getByLabelText(/email/i), "test@example.com"); // Simulates realistic typing
await user.click(screen.getByRole("button", { name: /submit/i }));
});
Common Interview Pitfalls
- Forgetting that `user-event` methods are asynchronous and must be awaited.
- Invoking `userEvent` methods without initializing `userEvent.setup()` at the start of the test case.
How does Mock Service Worker (MSW) simplify network API mocking in React tests?
Direct Answer
MSW intercepts network requests at the browser/node level using service workers or request interception, returning mocked responses instead of mocking fetch methods.
Detailed Explanation
Instead of mocking fetch methods globally (global.fetch = jest.fn()), which couples tests to specific request client implementations, MSW intercepts HTTP requests at the network layer:
http overrides).http.get("/api/users", () => HttpResponse.json(...))) which work seamlessly in unit tests, Storybook, and local development, ensuring integration validity.Code Example
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
const server = setupServer(
http.get("/api/users", () => HttpResponse.json([{ id: 1, name: "Alice" }]))
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Common Interview Pitfalls
- Forgetting to call `server.resetHandlers()` between tests, causing mocks configurations to leak into other test suites.
- Declaring relative API paths in MSW handlers that do not match the complete mock server fetch origin.
How do you handle browser Storage APIs in React and synchronize state changes across tabs?
Direct Answer
Access Storage APIs inside `useEffect` or `useSyncExternalStore`. Listen to the `storage` event to synchronize changes across different tabs.
Detailed Explanation
Using localStorage or sessionStorage in React requires handling SSR and multi-tab synchronization:
1. SSR Safety: Storage APIs are missing on the server. Always access them inside useEffect or verify typeof window !== "undefined" first.
2. Synchronize Tabs: When state changes in one tab and is saved to localStorage, other open tabs are unaware of the change. To synchronize them, listen to the global storage event (window.addEventListener("storage", callback)). This event fires in *all other* tabs when a storage key is modified, allowing them to sync their local state automatically.
Code Example
// Sync State across tabs
useEffect(() => {
const handleSync = (event) => {
if (event.key === "theme") {
setTheme(event.newValue);
}
};
window.addEventListener("storage", handleSync);
return () => window.removeEventListener("storage", handleSync);
}, []);
Common Interview Pitfalls
- Reading from `localStorage` directly in the component initializer in Next.js, causing instant hydration errors.
- Assuming the `storage` event fires in the same tab that triggered the write operation (it only fires in sibling tab frames).
Official Documentation & Specifications
JavaScript & TypeScript
React Fundamentals
React Hooks
State Management & Context
Performance Optimization
Want to tailer your resume for React Developer roles?
Import your resume, scan it for critical React Developer keywords, and compare it against ATS standards instantly.