React in Depth

React Interview Questions With Model Answers

The questions asked at every level: rendering, keys, hooks, state, performance, and the traps, with first-person answers.

Lesson 0918 min read#react#interview

On this page

  1. Words used in this lesson
  2. How to answer: a short method
  3. Level 1: the basics everyone must get right
  4. Level 2: how React really works
  5. Level 3: data, state and context
  6. Level 4: performance
  7. Level 5: patterns, server, testing and design
  8. Proofs for the traps
  9. Traps that interviewers love, in one table
  10. Try it: a mock interview
  11. Key takeaways
  12. What's next

In one line: a React interview is not about naming hooks. It tests whether you understand what happens when state changes (render, compare, commit), why the rules exist, and how you reason about data, performance and tests. This lesson has 38 questions from beginner to senior, with first-person model answers, and the traps that catch even experienced people. Many answers point to a small run, so you can say "I tried it" in the room.

Interviewers use a pattern. They start with something simple ("what is state?"), then push on the detail ("why did my update not show?"), then ask you to design something ("an autocomplete"). The people who do well are not the ones who memorised the most. They are the ones who can explain the mechanism in short, plain sentences and say "here is how I would check it".

This is lesson 9, the last of a ten-lesson series. Every topic here was taught in lessons 0 to 8: Thinking in React, State and Rendering, Effects, Hooks in Depth, Forms, Data Fetching, Context and State, Performance and Patterns, Server Components and Testing. If an answer feels thin, go back to that lesson. For wider front-end rounds, see React Rendering Model: Render, Commit, Reconciliation and Keys and Hooks in Depth: Effects, Refs and the Rules.

What I ran / checked. Linux, Node.js v22.22.0, react 19.3.0, react-dom 19.3.0, @testing-library/react 16.3.3, @testing-library/user-event 14.6.7, vitest 5.0.3 with jsdom. Every "I tried it" line in the answers below comes from a real test I ran for this lesson, and I pasted the output in the section "Proofs for the traps". Sources I read (own words): the React docs pages for memo, useMemo, useCallback, use, Suspense, useTransition, useDeferredValue, lazy, Component (error boundaries), "Choosing the State Structure", "You Might Not Need an Effect", "Passing Data Deeply with Context", "Scaling Up with Reducer and Context", the React Compiler docs, and the Server Components pages (raw markdown from reactjs/react.dev), plus TanStack Query's "Important Defaults", the Zustand README and the Testing Library docs. Could not do: I did not run a real Server Components framework, and I did not use the React DevTools extension here. Where an answer relies on those, it comes from the docs, and I say so. These are model answers. Interviewers differ, so use them as a shape for your own words, not a script. I give no star counts.

Words used in this lesson

  • Reconciliation is how React compares the new description of the UI with the old one to decide what to change in the DOM.
  • Render phase is when React calls your components. It must be pure. Commit phase is when React applies changes to the DOM and runs layout effects and effects.
  • Pure means the same inputs give the same output, with no side effects while rendering.
  • A closure is a function that remembers the variables around it when it was created.
  • A stale closure is a function that still sees old values from an earlier render.
  • Hydration is React attaching event handlers to HTML that the server already produced.
  • Idempotent here means: running something twice has the same result as once. StrictMode tests this for effects.
  • A race condition is a bug where the result depends on which of two things finishes first.
  • Referential equality means two values are the very same object in memory.

How to answer: a short method

  1. Answer in one sentence first. Then explain.
  2. Give the mechanism, not only the rule. "Hooks must be called in the same order because React matches them by position."
  3. Give a tiny example or a number.
  4. Say the trade-off. Most senior questions have "it depends". Say what it depends on.
  5. Say how you would verify. "I would check with the Profiler" or "I tried it in a test".

Level 1: the basics everyone must get right

Q1. What is React, and what problem does it solve? (Must know)

React is a JavaScript library for building user interfaces from components. The main idea is that the screen is a function of state: I describe what the UI should look like for the current data, and React works out how to update the page when the data changes. I do not tell the browser step by step what to change. This replaces a lot of manual DOM code that is easy to get out of sync. React is a library, not a framework: it does the view, and I pick the router, data fetching and build tool, or I use a framework such as Next.js that picks them for me.

Q2. Is the virtual DOM what makes React fast? (Trap)

Not by itself, and "the DOM is slow, the virtual DOM is fast" is a half truth. React builds a lightweight description of the UI (a tree of plain objects) on every render, compares it with the previous one, and changes only the parts of the real DOM that differ. That saves me from writing the update code and avoids needless DOM work. But rendering and comparing also cost time, and a naive full re-render of a big tree can be slower than a hand-written update. React's value is mostly a simple model with good-enough speed, and I fix real slowness by measuring (lesson 7): fewer renders, fewer DOM nodes, less code shipped.

Q3. What is JSX? (Must know)

JSX is syntax that looks like HTML inside JavaScript. It is not understood by the browser. A build tool turns <Button color="red">Hi</Button> into a function call that creates a plain object describing that element. Because it is just JavaScript, I can use {expressions}, map, and ternaries in it. Two details I mention: attributes use camelCase names such as className and onClick, and a component must return one root (a fragment <>...</> works).

Q4. What is the difference between props and state? (Must know)

Props are the inputs a component gets from its parent. They are read-only for the child. State is memory that belongs to the component and that it can change, and changing it makes React render again. Data flows down through props, and changes go up through callbacks that the parent passes down. A good test: if a value can be calculated from props or other state, it should be neither new state nor a copy of props. It should just be a variable calculated in render.

Q5. Why can't I change state directly, like list.push(x)? (Must know)

Because React decides whether to re-render by comparing the new state value with the old one by identity. If I mutate the array and call setList(list), it is the same reference, so React sees no change and skips the update. I tried it: after list.push("b"); setList(list) the screen still showed a and the render count stayed at 1. Then I called setList([...list, "c"]) with a new array and it showed a,b,c. So I always create a new array or object (spread, map, filter), which also keeps old state intact for things like memoization and time-travel debugging.

Q6. What happens if I call setCount(count + 1) three times in a row? (Trap)

The count goes up by 1, not 3. Inside one render, count is a fixed snapshot value, say 0. All three calls say "set to 0 + 1", so the last one wins with 1. I ran it: setN(n + 1) three times gave A:1, while setN(c => c + 1) three times gave B:3. The functional form (c => c + 1) receives the latest pending state, so the updates chain. I use it whenever the new state depends on the old state. Related: updates are also batched, so the three calls give one render, not three.

Q7. What are keys and why do they matter? (Must know)

A key gives each item in a list a stable identity, so React can match old and new items instead of matching by position. It should be unique among siblings and stay with the same data, such as a database id. If I use the array index and the list changes order or I add an item at the start, React matches the wrong items and keeps state in the wrong place. I tried it: I typed "typed-in-first" into the first row's input, then prepended a new row. With key={index} the text ended up in the new first row (NEW="typed-in-first"). With key={item.id} it stayed with the "first" row, where it belongs. Index keys are fine only for a list that never reorders, inserts or deletes.

Q8. Controlled versus uncontrolled inputs? (Must know)

A controlled input has its value in React state: value={text} and onChange updates it, so React is the source of truth. An uncontrolled input keeps its value in the DOM, and I read it when needed, with a ref or FormData on submit. Controlled is good when I need to validate or format on every keystroke, or to change one field from another. Uncontrolled is simpler for a plain form submitted once, and avoids a render per keystroke. A trap is switching an input between undefined and a value, which changes it from uncontrolled to controlled and React warns. I start with useState(""), not useState().

Q9. What does {count && <Items />} render when count is 0? (Trap)

It renders the number 0 on the page. In JavaScript 0 && x is 0, and React shows numbers. Booleans, null and undefined render nothing, but 0 and NaN do. I ran it and got <div>0</div>. The fix is to make the condition a real boolean: {count > 0 && <Items />} rendered <div></div>. Or use a ternary. This bug is common with .length.

Q10. Why do hooks have rules? (Must know)

The rules: call hooks only at the top level of a component or a custom hook, never inside conditions, loops or nested functions, and only from React functions. The reason is that React does not know hooks by name. It remembers them by order of calls in each component. On every render it expects the same hooks in the same order, so the second useState call always gets the second slot. If I call a hook inside an if, on a render where the condition is false the order shifts and every later hook gets the wrong state. The lint rule react-hooks/rules-of-hooks catches this. (use() in React 19 is the exception: it may be called in conditions, but it is not a normal hook.)

Q11. What is useEffect for, and what are the dependencies? (Must know)

useEffect lets a component synchronise with something outside React after rendering: a subscription, a timer, a browser API, a network connection. It runs after the screen is updated. The dependency array lists the reactive values the effect reads. React re-runs the effect when one of them has changed since the last render, and runs the cleanup function first. An empty array means "only on mount", and no array means "after every render". I tell the interviewer what an effect is not for: calculating values from props or state (do it in render), and responding to a user event (do it in the handler). Most "bad effects" are one of those two.

Q12. Why does my effect run twice in development? (Trap)

Because of StrictMode. In development React mounts the component, runs the cleanup, and mounts again, to prove the effect can be stopped and started safely. It only happens in development. The answer is not to remove StrictMode. I fix the effect: add the missing cleanup, or move one-off work such as "create the order" to an event handler. In my data fetching test, one component under StrictMode sent 2 requests, and the ignore flag made the first answer harmless.

Level 2: how React really works

Q13. Explain the render and commit phases. (Must know)

A render means React calls my component functions to get the new description of the UI. It is supposed to be pure and may be run more than once, paused or thrown away. Then React compares the result with the previous tree. The commit is when React applies the differences to the real DOM, and then runs useLayoutEffect callbacks, then lets the browser paint, then runs useEffect callbacks. I checked the order: render -> layoutEffect -> effect. So a render is not the same as a DOM update. A component can render and cause no DOM change at all, and that is fine. The practical consequence is that I never put side effects (a fetch, a write to localStorage) in the render body.

Q14. What is the difference between useEffect and useLayoutEffect? (Senior signal)

Both run after React has updated the DOM. useLayoutEffect runs before the browser paints, and blocks painting until it finishes, so I use it when I must measure the DOM and change something before the user sees a frame, such as positioning a tooltip, to avoid a flicker. useEffect runs after paint and does not block it, so it is the default. A slow layout effect makes the page feel janky. Layout effects also do not run on the server.

Q15. When does a component re-render? (Must know)

Three reasons. It renders for the first time. Its own state changed, or a context it reads changed. Or its parent rendered, in which case all its children render too, even if their props are identical. The third reason is the surprising one. It is normal and usually cheap, and it only needs fixing when it is slow or when many components are involved. Props changing is not a separate reason: props change because a parent rendered.

Q16. Are state updates synchronous? What is batching? (Must know)

Calling setState does not change the variable in the code that is running. It schedules a render, and the next render gets the new value. React batches updates, so several setState calls in one event produce one render. Since React 18 this also applies inside timeouts, promises and native events, not just React event handlers. I tried two setState calls inside a setTimeout and counted: 1 render. If I need to read the DOM right after an update, I use an effect or flushSync, which is rarely needed.

Q17. What is a stale closure? Give an example. (Senior signal)

A function created in one render keeps the values from that render. If it lives longer than the render, for example inside a setInterval set up in an effect with an empty dependency array, it keeps seeing the old values. I tried it: the button showed inc:1, but the interval callback logged n=0, because it was created when n was 0. The fixes: list the value in the dependency array so the effect re-subscribes, use the functional update setN(c => c + 1) when I only need to update, or keep the latest value in a ref when I need to read the current value without re-subscribing.

Q18. useRef versus useState? (Must know)

Both keep a value between renders. Changing state triggers a re-render. Changing ref.current does not. I use state for anything that appears on the screen. I use a ref for values that do not affect the output: a timer id, a previous value, a DOM node. I tried it: two changes to ref.current gave 0 renders and one setState gave 1. I never read or write ref.current during render, since that makes the component impure.

Q19. Why is it a bad idea to define a component inside another component? (Trap)

Each time the outer component renders it creates a new function, which React treats as a new component type. So it unmounts the old inner component and mounts a new one, destroying its DOM and state. I tried it: I typed "hello" into an input inside an inner component and then re-rendered the parent, and the input was empty afterwards. I define components at the top level of the module. If I need to configure one, I pass props.

Q20. How can a key be used to reset a component's state? (Senior signal)

When a key changes, React treats it as a different element: it unmounts the old one and mounts a new one with fresh state. So <Form key={userId} /> resets the form when the user changes, without an effect that clears fields. I tried it: after typing "abc" and switching the id, the input without a key still held "abc", and with key={id} it was "". This is cleaner than syncing state with an effect. The React docs recommend it for resetting state. The same trick resets an error boundary.

Q21. Why is copying props into state a problem? (Trap)

useState(props.name) uses the prop only as the initial value. If the prop changes later, the state does not follow, so the screen shows an old value. The same happens with derived data stored in state, such as fullName built from firstName and lastName. I avoid it by calculating in render, or if I want a form to start from a prop and then diverge, I name it initialName and reset it with a key. The React docs list this as "avoid redundant state".

Level 3: data, state and context

Q22. How do you fetch data in a component safely? (Must know)

With useEffect I need a cleanup, because of the race condition: if the id changes quickly, an old slow response can arrive last and overwrite the new one. I set an ignore flag in the cleanup, or use an AbortController and ignore the AbortError. I check res.ok, because fetch does not reject on 404 or 500. And I keep a single status for loading, error and success. In my test the screen without cleanup ended with "user 2: profile of user 1". In a real project I prefer a cache library such as TanStack Query, or my framework's loader, because they also solve sharing, caching, retry and refetching.

Q23. staleTime versus gcTime in TanStack Query? (Senior signal)

staleTime is how long data counts as fresh, so no refetch is triggered while it is fresh. The default is zero. gcTime is how long unused data stays in memory after the last component stops using it, 5 minutes by default (older versions called it cacheTime). I show it with a test: with staleTime: 60_000, a third component mounting later caused no extra request: the fetch count stayed at 1. A related point: everything the query function reads must be in the queryKey, as with effect dependencies.

Q24. When would you use context, and what is its cost? (Must know)

For values many distant components need that change rarely: theme, current user, language. The cost is that every component reading the context re-renders when its value changes, and memo does not stop that. I measured this: two memo-wrapped labels read one big context, I changed only one field, and the other label rendered too. I reduce it by splitting contexts by how often they change, putting dispatch in its own context, memoising the provider value (2 useless renders became 0 in my test), or using a store with selectors for fast-changing shared state. Context is also not state by itself: the state comes from useState or useReducer.

Q25. When would you pick Redux Toolkit or Zustand over context? (Senior signal)

When shared client state changes often and many components read different slices of it, or when I need to read and write it outside React, or want devtools and middleware. Both support selectors, so a component re-renders only when its slice changes. In my test, with a selector on a, only the component reading a rendered after incA(). I choose Zustand for small to medium apps and quick setup, and Redux Toolkit for large teams that want one strict pattern, plus its RTK Query. I add that I would not put server data into either: that belongs in a query cache.

Q26. Where should each piece of state live? (Must know)

As low as possible. One component: local state. Siblings that must agree: lift to the parent. Calculable: do not store it. Many distant readers that rarely change: context. Server data: a query cache. Frequently changing shared client state: a store with selectors. Anything that should survive reload or be shared as a link, like filters and page numbers: the URL. I move state up only when forced.

Q27. useReducer or useState? (Must know)

useState for simple independent values. useReducer when one state has several fields that change together, or many kinds of update, or when the next state depends on the old state in complex ways. The reducer is a pure function (state, action) => newState, which I can test without React. With context, I put the state in one context and dispatch in another, since dispatch never changes identity, so components that only send actions do not re-render on state changes.

Level 4: performance

Q28. A page is slow. What do you do? (Senior signal)

I measure before I change anything. I reproduce it in a production build with CPU throttling, record with the React DevTools Profiler (or the <Profiler> component or a render counter), and classify the problem: too many DOM nodes, too much JavaScript on load, wasted re-renders, one slow calculation, or an update that is slow but necessary. Fixes in order of cost: move state down or pass children, stabilise props and use memo, useMemo for a slow calculation, virtualise a long list, split code with lazy, and mark slow updates as transitions. Then I measure again, and if the numbers do not improve, I revert.

Q29. Why does React.memo sometimes not work? (Trap)

Because it compares props shallowly, and a new object, array or function created in the parent's render has a new identity each time. I measured 5 rows and 3 keystrokes: plain gave 15 row renders, memo with an inline handler still gave 15, and memo with a useCallback handler gave 0. memo also cannot stop a render caused by the component's own state or a context. And it is a hint: React may still render. I also mention a custom arePropsEqual must compare every prop, including functions, or I will get stale closures.

Q30. How do you render a list of 10,000 items? (Senior signal)

I first ask whether the user needs 10,000 rows. If yes, I virtualise (windowing) with a library such as react-window, so only the visible rows plus a small buffer exist in the DOM. In my Chromium test the full list produced 50,006 DOM nodes and took 139 to 627 ms to render and up to about 1.8 s to paint, and the virtualised list had 115 nodes and took 15 to 28 ms. The numbers varied between runs on my sandbox, but the node counts did not. The costs are fixed or known row heights, and weaker find-in-page and accessibility. Otherwise I paginate or filter on the server.

Q31. useTransition versus useDeferredValue? (Senior signal)

Both mark work as not urgent, so typing stays responsive. useTransition wraps the state update I make (startTransition(() => setTab(x))) and gives isPending. useDeferredValue wraps a value and returns a lagging copy, good when I do not control where the value is set. The text input itself must stay urgent state. In my test, a slow result list with useDeferredValue rendered 4 to 8 times for 8 keystrokes against a constant 8 with the plain version, and typing finished faster. I stress it is about responsiveness and not a guaranteed drop in renders, and that it does not make a slow render faster.

Q32. What does the React Compiler do? Do I still need useMemo? (Senior signal)

It is a build-time Babel plugin that analyses components and hooks and adds memoization automatically, using a per-component cache. In my test, an input plus five rows with an inline function prop and no memo anywhere went from 15 row renders to 0 after compiling, with no source change. It relies on the Rules of React, so code that mutates state or breaks purity may be skipped or misbehave. It memoizes components and hooks, not arbitrary helpers, and it does not fix long lists or bundle size. The docs advise keeping existing useMemo and useCallback and using them in new code only where I need exact control, for example a stable effect dependency. So I still need to understand them.

Level 5: patterns, server, testing and design

Q33. What do error boundaries catch? (Must know)

Errors thrown while rendering their children, in lifecycle methods and in constructors. They do not catch errors in event handlers, in async code like setTimeout or plain promises, in server rendering, or in the boundary itself. The exception is an error thrown inside startTransition, which is caught. I ran each case: render error caught with the sibling still on screen, click and timer errors not caught, transition error caught. A boundary must be a class component (getDerivedStateFromError), so I write one wrapper or use react-error-boundary. I reset one by changing its key.

Q34. What are Server Components, and how are they different from SSR? (Senior signal)

SSR runs all components on the server once to produce the first HTML, and then the browser hydrates them, so the same code also runs in the browser. Server Components run only on the server (or at build time) and their code is never sent to the browser. They can be async, read a database directly and use heavy libraries for free, but cannot use state, effects or event handlers. 'use client' marks the file where client code starts. There is no directive for Server Components, and 'use server' marks Server Functions, which the client can call over the network and must validate like public endpoints. Props crossing from server to client must be serializable. This answer comes from the React docs, because I did not run a Server Components framework.

Q35. How do you test a React component? (Must know)

With Testing Library, Vitest or Jest and jsdom. I render the component, find elements as a user does (getByRole with a name first, then label, then text), act with userEvent, and assert on visible results with jest-dom matchers. I use findBy for things that appear later and queryBy to check something is absent. I mock the network and timers, not my own child components, and I do not test state variables or internal calls. In my signup form example, three behaviour tests passed and would survive a rewrite of the component's inside. For what jsdom can't see (layout, focus rules), I keep a few end-to-end tests in a real browser.

Q36. What is hydration, and what is a hydration mismatch? (Senior signal)

Hydration is React attaching event handlers and state to HTML that the server already produced, by running the same components in the browser and matching them to the existing DOM. A mismatch happens when the first browser render differs from the server HTML, usually because render depends on something that is different there: Date.now(), Math.random(), window.innerWidth, or localStorage. I fix it by rendering the same thing first and updating after mount in an effect, or by keeping browser-only parts out of the first render. In my renderToString run, the HTML had no click handler and the effect did not run, which shows why the browser must hydrate.

Q37. Design an autocomplete (search-as-you-type) component. (Senior signal)

I would describe it in parts. Input is controlled state. Fetching: a debounced value (about 200 to 300 ms) feeds a query keyed by the text, with an abort or cache so old responses cannot overwrite new ones. The results list shows loading, empty and error states. For slow results I can use useDeferredValue so typing stays smooth. State: text and highlighted index are local; results come from the query cache. Accessibility: role="combobox" with aria-expanded and aria-activedescendant, arrow keys and Enter, Escape to close. A portal for the dropdown if clipping is a risk. Performance: virtualise if results are very long. Testing: type with userEvent, findByRole("option"), press arrows, assert selection. I would also ask about the data size and how the API is rate limited.

Q38. How would you design a reusable Modal or Tabs component API? (Senior signal)

I'd use composition and ask who owns the state. For Tabs, a compound component (Tabs, Tabs.Tab, Tabs.Panel) sharing state through context, so the user arranges the parts as they like. I'd support both controlled (value, onValueChange) and uncontrolled (defaultValue) usage. For Modal, I'd use a portal, children, an onClose callback, focus moved in on open and returned on close, Escape to close, and the native <dialog> where possible. I would avoid a long list of boolean props and avoid cloning children. I'd also write the accessible role and keyboard behaviour first, and test by role.

Proofs for the traps

You can paste these into a Vitest file with @testing-library/react, user-event and jsdom to repeat them. The outputs are real runs on React 19.3.0.

TEXT
setN(n+1) x3 -> A:1 | setN(c=>c+1) x3 -> B:3after mutate+same ref: a (renders 1)after new array:      a,b,c (renders 2)key=index  -> NEW="typed-in-first"  first=""  second=""key=id     -> NEW=""  first="typed-in-first"  second=""typed: hello -> after parent re-render: ""button shows inc:1 but interval sees n=0{0 && <span/>} renders: "<div>0</div>"{count > 0 && ...} renders: "<div></div>"without key -> input after clicking next: "abc"with key={id} -> input after clicking next: ""two ref changes -> renders: 0 | one state change -> renders: 1two setStates in a setTimeout -> renders: 1render -> layoutEffect -> effect

How to read the groups, top to bottom: batching and the updater form (Q6), mutating state (Q5), keys in lists (Q7), a component defined inside a component (Q19), a stale closure (Q17), the 0 && trap (Q9), key as reset (Q20), refs against state (Q18), batching inside a timeout (Q16), and effect order (Q13).

One more honest note about the stale closure line: I clicked the button once, so n became 1 on screen. The interval was created when n was 0 and its callback never saw the new value.

Traps that interviewers love, in one table

TrapShort truth
"The virtual DOM is why React is fast."It is a model that saves manual update code. Speed comes from fewer renders and fewer nodes.
"setState updates the value now."It schedules a render. The next render sees the new value.
"setCount(count+1) three times adds 3."It adds 1. Use c => c + 1.
"Use index as key, it is fine."Only if the list never reorders or inserts.
"0 && <X /> renders nothing."It renders 0.
"Effects run once."Not in dev: StrictMode runs them twice on purpose.
"fetch rejects on 404."It does not. Check res.ok.
"memo stops all re-renders."Not for state or context changes, and an inline function prop defeats it.
"Context is a state manager."It transports a value. State comes from useState or useReducer.
"Error boundaries catch every error."Only render errors (plus errors in startTransition).
"'use server' makes a Server Component."It marks Server Functions. There is no directive for Server Components.
"Server Components replace SSR."Different things, used together.
"The compiler means I can forget useMemo."Know it: you still read old code and may need exact control.
"Put API data in Redux."Keep it in a query cache.

Try it: a mock interview

Answer each out loud in about 40 seconds. Record yourself if you can. Then compare with the key points.

  1. Explain what happens from setCount(c => c + 1) to the pixels changing.
  2. Why is key={index} dangerous? Show a case.
  3. A teammate says "my useEffect fetch shows the wrong user sometimes". What is the likely cause and your fix?
  4. The profile page re-renders every time the user types in the search box in the header. List three ways to stop it, cheapest first.
  5. A table needs 20,000 rows. What do you do?
  6. Your error boundary does not catch an error thrown in an onClick. Why? What do you do instead?
  7. Explain Server Components to a colleague who only knows useEffect fetching.
  8. Write a Testing Library test, in words, for a login form with a failing server.

Answers

  1. The call schedules an update with a function. React batches it with other updates, then renders: it calls my component, which reads the new state, and returns a new tree. React compares it with the old tree, commits the differences to the DOM, runs layout effects, the browser paints, and then effects run.
  2. React matches list items by key. With index keys, inserting at the start shifts every item, so state such as an input's text stays at the position, not with the data. In my test the typed text jumped to the new first row. Use a stable id.
  3. A race condition: an old request finishes after a newer one. Fix with a cleanup (ignore flag or AbortController), or use a query library that keys data by id.
  4. Move the search state down into the header (or pass the page as children), so the page is not its child. Stabilise props and wrap the page's heavy parts in memo. Or enable the React Compiler. Measure first with the Profiler.
  5. Ask whether all rows are needed. Else virtualise with fixed row heights (my test: 50,006 nodes down to 115). Or paginate or filter on the server. Keep sorting and filtering on the server if the data is huge.
  6. Boundaries catch rendering errors only. Event handler errors do not break rendering. I use try/catch in the handler, set an error state, and show it. Or rethrow during render to reach the boundary.
  7. Server Components are components that run on the server (or at build time) and send only their rendered output, so they can read the database directly and ship no code for it. No second request after load, and no waterfall for that data. They cannot hold state, so interactive parts are Client Components marked with 'use client', and props between them must be serializable.
  8. Render the form with a fake onSubmit that rejects. Type an email with userEvent, click the button by role and name, then await findByRole("alert") and check its text. Also assert the button is enabled again. No test ids and no checks on internal state.

Key takeaways

  • Explain mechanisms, not slogans: render then compare then commit, state as a snapshot, hooks by call order, keys as identity.
  • Have a number or a run for your claims: 1 versus 3 on updates, 15 renders versus 0 for memo and the compiler, 50,006 versus 115 DOM nodes.
  • Know the standard traps: index keys, 0 &&, mutated state, stale closures, defining components inside components, fetch and 404, effects twice in development, error boundaries and event handlers.
  • Choose state placement by a rule: local first, lift when forced, derive when you can, cache server data, URL for shareable state.
  • For performance, measure, fix the cheapest cause first, and measure again.
  • For Server Components, say what runs where, what 'use client' and 'use server' really mean, and say clearly if your experience comes from docs only.
  • For tests, behave like a user: roles, userEvent, findBy, mock the edges.

What's next

This is the end of the React series. To keep going, read the rendering and hooks deep dives in Frontend Depth, Lesson 0 and Lesson 1, the Server Components side in Lesson 2, and the testing series starting at Testing UI Components with Testing Library. For TypeScript with React props and generics, see Advanced TypeScript for Interviews. Practise by answering the eight mock questions out loud, then pick any five questions above and explain them to a friend without looking.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Part of the study guide

Related series