
React interviews in 2026 still begin with the basics: JSX, props, state and keys. But they don't stop at useEffect any more. You will be asked why React re-renders, when memoisation actually helps, and how React 19 features like Actions, useActionState, useOptimistic, use() and Server Components change the way you build forms and fetch data.
Below are 40 questions that keep coming up, ordered from basics to advanced. Each answer is short enough to say out loud in an interview. Each one also explains why React works that way, because the next question is almost always "why?".
1. Fundamentals
Q1. What is React, and what does "declarative" mean?
React is a JavaScript library for building UIs out of components. It is declarative: you describe what the UI should look like for a given state, and React figures out which DOM changes are needed. With imperative DOM code you write every step yourself (find this node, change its text, add this class). That gets hard to keep correct as the number of states grows. In React, the UI is a function of state. Change the state and React updates the screen to match.
Q2. What is JSX, and what does it compile to?
JSX is a syntax extension that lets you write markup inside JavaScript. It is not HTML. A compiler like Babel, SWC or esbuild turns each tag into a function call. With the modern automatic runtime, <h1 className="title">Hi</h1> becomes roughly jsx('h1', { className: 'title', children: 'Hi' }), imported from react/jsx-runtime. Older setups used React.createElement. This explains a few things. JSX uses className and htmlFor, you can put any expression inside {}, and a component must return a single root (or a Fragment), because it is one function call returning one value.
Q3. What is the difference between a React element and a component?
A component is a function (or class) that takes props and returns UI. An element is a plain, immutable object that describes one piece of that UI, like { type: 'button', props: {...} }. When you write <Button />, you create an element whose type is the Button function. React calls that function later, while rendering. Elements are cheap and get thrown away after each render. Component instances (and their state) are kept by React between renders.
Q4. What is the difference between props and state?
Props are inputs passed in from a parent, and the child must treat them as read-only. State is data a component owns and changes over time. Updating state schedules a re-render. This split keeps data flow predictable: every piece of data has exactly one owner that can change it, and everything below gets it as props. If two components need the same changing data, move the state up to their closest common parent.
Q5. What are controlled and uncontrolled components?
In a controlled input, React state is the source of truth. You pass value and update state in onChange, so every keystroke goes through React. In an uncontrolled input, the DOM holds the value. You set the starting value with defaultValue and read the value when you need it, through a ref or from FormData on submit. Controlled inputs make live validation and dependent fields easy. Uncontrolled inputs are simpler and fit well with React 19 form Actions, which get a FormData object directly.
Q6. Why does React need keys in lists?
Keys help React match items between renders. Without keys, React compares children by position. Insert an item at the top and every row looks "changed", and state like typed text or a ticked checkbox can end up on the wrong row. A key should be stable and unique among siblings, usually an id from your data. Array indexes are safe only for lists that never reorder, insert or filter. Generating keys during render (Math.random()) makes every item remount on every render.
Q7. What is the virtual DOM, and what does "rendering" mean in React?
Rendering means React calls your component functions to get a new tree of elements. That tree is what people call the virtual DOM. React compares it with the previous tree (reconciliation) and then applies only the differences to the real DOM in the commit phase. So rendering is not the same as updating the screen. A component can render, produce the same output, and the DOM is not touched at all. For the full cycle, see How React Rendering and Reconciliation Work.
2. Hooks
Q8. What are the Rules of Hooks, and why do they exist?
Call hooks only at the top level of a component or a custom hook. Never call them inside conditions, loops or nested functions, and only call them from React functions. The reason is how React stores hook data. It keeps a list of hook slots for each component and matches them by call order on every render. If a condition skips one useState call, every hook after it reads the wrong slot. The ESLint plugin eslint-plugin-react-hooks checks these rules for you. The one exception is use() in React 19, which can be called conditionally.
Q9. Why does reading state right after setting it give the old value?
State works like a snapshot. Each render gets its own fixed values. Calling the setter schedules a new render. It does not change the variable in the current one. After setCount(count + 1), count still holds the old number until the next render. If the new value depends on the old one, use the updater form. React applies these in order, each one on the latest queued value:
setCount(c => c + 1);
setCount(c => c + 1); // count goes up by 2
Q10. How do the useEffect dependency array and cleanup work?
useEffect keeps a component in sync with something outside React, like a subscription, a timer or a network connection. It runs after the commit. With no array it runs after every render, with [] only after mount, and with [a, b] whenever a or b changes (checked with Object.is). The function you return is the cleanup. React runs it before the effect runs again and when the component unmounts, which prevents leaks and stale subscriptions. Leaving a value out of the array doesn't just make the effect run less. It makes it read stale values. That is why the lint rule asks for every value the effect uses.
Q11. What is the difference between useEffect and useLayoutEffect?
Both run after React has updated the DOM. useLayoutEffect runs synchronously before the browser paints. So it can measure layout and adjust it before the user sees any flicker, for example when positioning a tooltip. useEffect runs after paint, so it does not block the frame. Use useEffect by default. Switch to useLayoutEffect only when you must read layout and change the DOM before paint.
Q12. When would you use useRef instead of useState?
Use a ref for a value that must survive re-renders but should not trigger one when it changes: a DOM node, a timer id, the previous value of a prop, or an AbortController. useRef returns the same mutable object { current } on every render, and changing current does not tell React anything. If the value shows up in the rendered output, it belongs in state. Otherwise the UI won't update when it changes. Don't read or write ref.current during rendering, because that makes rendering impure.
Q13. What is the difference between useMemo and useCallback?
useMemo(() => compute(a), [a]) caches the result of a calculation. useCallback(fn, [a]) caches the function itself. In fact, useCallback(fn, deps) is the same as useMemo(() => fn, deps). They solve two problems: skipping expensive recalculations, and keeping object and function references stable so a memo child or an effect dependency doesn't see a "new" value on every render. They are not free, so use them only when one of those problems is real. With the React Compiler turned on, most of this memoisation is added for you.
Q14. When should you choose useReducer over useState?
Pick useReducer when the next state depends on the previous state in several different ways, or when many fields change together, like a multi-step form or a shopping cart. All the update logic moves into one pure function, (state, action) => newState. That is easy to read and easy to unit test without rendering anything. Event handlers then only say what happened (dispatch({ type: 'added', item })), not how the state changes. For one or two independent values, useState is simpler.
Q15. What is a custom hook, and does it share state between components?
A custom hook is a function whose name starts with use and which calls other hooks, like useOnlineStatus() or useDebouncedValue(). It shares logic, not state. Every component that calls it gets its own separate copy of the state inside. To share the same data between components, lift the state up, put it in context, or use an external store. The use prefix matters because it tells React and the linter that the Rules of Hooks apply inside.
3. State and data flow
Q16. What does "one-way data flow" mean, and what is lifting state up?
In React, data flows down. Parents pass props to children, and children cannot change the parent's data directly. If a child needs to affect the parent, the parent passes down a callback and the child calls it. When two siblings need the same data, you move the state up to their closest common parent and pass it to both. That is lifting state up. You get a single source of truth, so the two siblings can never disagree.
Q17. What is prop drilling, and is Context the solution?
Prop drilling means passing props through several layers of components that don't use them, just to reach a deeply nested child. Context fixes this by letting any descendant read a value from the nearest provider. The catch: every component that reads the context re-renders when the provider's value changes. So Context works best for values that rarely change, like theme, locale or the current user. Before reaching for Context, check if composition, such as passing a component as children, removes the drilling more simply.
Q18. Why should you avoid storing derived values in state?
If a value can be calculated from existing props or state, like a filtered list or a full name, store only the inputs and calculate the result during render. Keeping a copy in state means you must keep the two in sync, usually with an effect. That costs an extra render and causes bugs when the sync is missed. Calculating during render is always correct and usually cheap. If it is expensive, wrap it in useMemo. The same goes for copying props into state: the copy won't update when the prop changes.
Q19. When do you need a state management library?
First, separate server state from client state. Server state is data fetched from an API. It needs caching, deduplication, refetching and invalidation, and libraries like TanStack Query, SWR or your framework's data loading do this far better than hand-written effects. Client state is UI state, like an open modal or a draft. It is usually fine in useState, useReducer and Context. A store like Redux Toolkit or Zustand is worth it when many distant components read and update the same client state often. Components can then subscribe to just the slice they need instead of re-rendering on every change.
Q20. Why must state be updated immutably?
React checks whether state changed by comparing the old and new values with Object.is. If you mutate an object or array and pass the same reference back, React sees no change and may skip the render. Immutability also keeps each render's snapshot correct, so memo, effects and dependency arrays behave as expected. Create new objects instead, for example setItems([...items, newItem]) or setUser({ ...user, name }).
4. Rendering and performance
Q21. What causes a component to re-render?
A component renders when its own state changes, when a context it reads changes, or when its parent renders. The last one surprises people. By default, when a parent renders, all its children render too, whether their props changed or not. Props only matter here when the child is wrapped in memo. That is why moving state down into the component that uses it, or passing static content as children, often fixes performance better than memoisation.
Q22. Why does switching between two different components reset their state?
Reconciliation compares element types at each position. If the type is the same, React keeps the existing instance and updates its props, so state is kept. If the type changes, React unmounts the old subtree (its state is lost) and mounts the new one. This explains two well-known effects. A component defined inside another component resets on every render, because it is a new function and so a new type each time. And changing a component's key is a deliberate way to reset its state.
Q23. What does React.memo do, and why does it sometimes not help?
memo(Component) skips a re-render when every prop is shallowly equal to the previous props. It fails when the parent passes a new object, array or function on every render, like style={{ margin: 8 }} or onClick={() => save(id)}. A new reference is never equal to the old one. Fix it by making those values stable with useMemo and useCallback, or let the React Compiler do it. And measure first in the React DevTools Profiler. Most renders are cheap and don't need memo.
Q24. What is automatic batching?
Batching means React groups several state updates into one render. Since React 18 this happens automatically everywhere: event handlers, timeouts, promises and native event listeners. Before React 18, only updates inside React event handlers were batched, so two setState calls inside a fetch().then() caused two renders. Batching is also why state doesn't change right after a setter call. React waits for your code to finish and then renders once.
Q25. What do useTransition and useDeferredValue do?
Both mark work as non-urgent, so urgent updates like typing stay responsive. useTransition returns [isPending, startTransition]. Updates inside startTransition render in the background, and React can interrupt and restart them if a newer urgent update comes in. useDeferredValue(value) gives you a copy of a value that lags behind during heavy rendering. It is handy when you don't control the code that sets the state. This works because concurrent rendering lets React pause a render and throw it away, which the old synchronous renderer could not do.
Q26. How do lazy and Suspense help performance?
React.lazy(() => import('./Chart')) splits a component into its own JavaScript chunk, which loads only when the component first renders. While it loads, the nearest <Suspense fallback={...}> boundary shows the fallback. This makes the initial bundle smaller, so the first page loads and becomes interactive faster. Suspense works for data too. In React 19 a component can suspend while reading a promise with use(), and frameworks stream Server Component output the same way.
5. Patterns and architecture
Q27. Why does React prefer composition over inheritance?
In React you reuse components by putting them inside each other and passing content through props, mostly children. A Card or Modal doesn't need to know what it wraps. It just renders {children} where the content goes. This is more flexible than class inheritance, because you combine behaviour where you use it instead of fixing it in a hierarchy. The React team has said they found no use cases at Facebook where component inheritance was the better choice.
Q28. How do higher-order components, render props and hooks compare?
All three share logic between components. A higher-order component wraps a component and injects props (withAuth(Page)). Render props pass a function that receives data (<Mouse render={pos => ...} />). Both add wrapper layers to the tree and can cause prop name clashes. Custom hooks share the same logic with a plain function call and no extra components. That is why hooks replaced the other two in most new code.
Q29. What is an error boundary, and what does it not catch?
An error boundary is a component that catches errors thrown while rendering its children. It shows a fallback UI instead of letting the whole app unmount. It still has to be a class component with static getDerivedStateFromError and usually componentDidCatch. There is no hook version, so many teams use the react-error-boundary package. It does not catch errors in event handlers, in async code like setTimeout, during server rendering, or in the boundary itself. Handle those with try/catch, or put the error into state so it gets thrown during render.
Q30. What is a portal, and how do events behave inside one?
createPortal(children, domNode) renders children into a different DOM node, usually document.body. This is useful for modals, tooltips and toasts that need to escape a parent's overflow: hidden or stacking context. The portal is still part of the React tree, so it still gets context from its React parents. Events also bubble through the React tree, not the DOM tree. A click inside a portal reaches onClick handlers on its React ancestors, even though the DOM node lives somewhere else.
Q31. What is the compound components pattern?
Compound components are a set of components that work together and share hidden state, like <Tabs>, <Tabs.List>, <Tabs.Tab> and <Tabs.Panel>. The parent holds the state and shares it through Context. Whoever uses them can arrange the pieces freely without passing props between them. The API feels like native <select> and <option>, and you see it a lot in component libraries like Radix UI.
6. React 19 and Server Components
Q32. What are Actions in React 19?
An Action is an async function that runs inside a transition. React tracks it for you. It sets a pending state while the function runs, lets you show optimistic updates, sends thrown errors to the nearest error boundary, and keeps overlapping submissions in order. You can pass an Action straight to a form with <form action={fn}>. The function receives the form's FormData, and after a successful submit React resets uncontrolled fields. A child component can read the form's pending status with useFormStatus from react-dom, for example to disable the submit button.
Q33. What does useActionState do?
useActionState wraps an Action and stores the result of its last run as state. You reach for it whenever a form needs to show a server error or a success message:
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const err = await updateName(formData.get('name'));
return err ?? null;
},
null
);
// <form action={submitAction}>...</form>
The action gets the previous state and the submitted data. Whatever it returns becomes the new state. This replaces the usual three useState calls for data, error and loading. In earlier canary releases it was called useFormState.
Q34. How does useOptimistic work?
useOptimistic(state, updateFn) returns [optimisticState, addOptimistic]. Inside an Action you call addOptimistic(value) to show the expected result right away, like a new message marked "sending" or a liked heart. While the Action runs, the UI shows the optimistic value. When it finishes, React drops that value and shows the real state again. If the request failed, the real state never changed, so the UI rolls back on its own with no manual undo code.
Q35. What is the use() API, and how is it different from a hook?
use(resource) reads the value of a promise or a context during render. With a promise, the component suspends until it resolves, so the nearest Suspense boundary shows its fallback. If the promise rejects, the nearest error boundary handles it. Unlike hooks, use can be called inside conditions and loops, because it doesn't depend on a fixed call order. The promise should come from a stable source, like one passed down from a Server Component or a caching library. A promise created inside a Client Component's render would be a new promise on every render.
Q36. What changed for refs and Context in React 19?
Function components can now take ref as a normal prop. New code doesn't need forwardRef, and it is expected to be deprecated later. Ref callbacks can return a cleanup function, which React calls when the element is removed, just like an effect cleanup. You can also render <ThemeContext value="dark"> directly instead of <ThemeContext.Provider>. Other additions: you can render <title> and <meta> tags anywhere in the tree (React moves them to the document head), and hydration error messages are clearer.
Q37. What are Server Components, and how do they relate to 'use client' and 'use server'?
Server Components render only on the server. They can be async, read databases or files directly, and add no JavaScript to the client bundle. But they cannot use state, effects or event handlers. 'use client' at the top of a file marks where Client Components start, and props crossing that boundary must be serializable. 'use server' does not mark Server Components. It marks Server Functions (Server Actions) that the client can call, like a form Action that writes to the database. The React Server Components guide covers the boundary in detail.
7. Testing and tooling
Q38. How do you test React components?
Most teams use React Testing Library with Jest or Vitest. The idea is to test behaviour the way a user sees it. Render the component, find elements by role, label or text (getByRole('button', { name: /save/i })), interact using user-event, and check what shows up on screen. Tests that skip implementation details like internal state or class names keep passing when you refactor. Querying by role also checks basic accessibility. For async UI, use findBy... queries, which wait for elements to appear, and mock the network with a tool like Mock Service Worker.
Q39. What does Strict Mode do, and why do effects run twice in development?
<StrictMode> turns on extra checks, in development only. It renders components twice to catch impure rendering. It also runs every effect's setup, cleanup and setup again on mount, to catch effects with missing cleanup. If an effect breaks when run twice (a duplicate subscription, an event listener added twice), it would also break when a user leaves the page and comes back. So the double run is showing you a real bug. None of this happens in production builds.
Q40. What is the React Compiler, and does it replace useMemo and useCallback?
The React Compiler is a build-time tool, used as a Babel plugin or through framework integrations. It reads your components and hooks and adds memoisation automatically. It memoises computed values, callbacks and JSX at a fine-grained level, so in most components you no longer write useMemo, useCallback or memo by hand. It needs your code to follow the Rules of React, like pure rendering and no mutation of props or state, and it skips components it can't safely optimise. Manual memoisation is still useful as an escape hatch, for example to keep an effect dependency stable on purpose.
How to prepare
- Always give the "why". For each answer above, practise one extra sentence on how it works underneath: snapshots, call order,
Object.is, type comparison during reconciliation. - Build one small app with React 19 features. A form using
useActionState,useOptimisticanduseFormStatusteaches more than reading about them. - Profile something. Open the React DevTools Profiler on a real page, find a component that renders too often, and fix it. Interviewers like concrete stories.
- Know the rendering cycle by heart. Trigger, render, reconcile, commit, effects. Most performance and "why did this happen" questions come back to it.
- Practise with timed MCQs. Short question sets expose gaps quickly; see the practice links below.
FAQ
Are class components still asked about in React interviews?
Less often. But you should know the main lifecycle methods and how they map to hooks, because many codebases still have class components. Error boundaries are the one place where you still write a class today.
Do I need to know Next.js for a React interview?
For most frontend roles in 2026, yes, at least the basics. You mostly use Server Components and Server Actions through a framework like Next.js, so interviewers often ask about both together.
Should I still mention useMemo and useCallback if the React Compiler exists?
Yes. Explain what they do and why, then add that the React Compiler can now do this memoisation for you. Showing that you understand the problem the compiler solves counts for more than either answer alone.
Comments
Post a Comment