Home › Technical › React

React deep dive

React is the heart of the role. The JD calls out React, TypeScript, performance optimization, complex state, and large data UI. Expect serious depth here in stage 3, lighter conceptual probing in stage 1.

01Rendering & reconciliation

How does React's rendering work? What's the virtual DOM?

React doesn't render directly to the DOM. When state or props change, the component function runs and returns a tree of plain objects describing what the UI should be — that's the virtual DOM. React compares this new tree to the previous one ("reconciliation"), figures out the minimal set of real DOM mutations needed, and applies them in a commit phase.

Two phases:

  • Render phase — pure, can be paused/restarted/discarded. Don't put side effects here.
  • Commit phase — synchronous, applies DOM changes, runs effects.

The virtual DOM is a useful abstraction, but the real win is the declarative programming model — you describe what the UI should look like at any state, React figures out how to get there.

What is reconciliation? How does the diffing algorithm work?

React's diffing is based on two heuristics that make it O(n) instead of O(n³):

  1. Different element types mean different trees — <div> replaced with <span> tears down the whole subtree.
  2. Lists use keys to identify which items moved versus which were added or removed. Without keys, React falls back to index-based diffing, which is wrong when items reorder.

This is why key matters and why using array index as a key is a bug — when items reorder, React can't tell what moved.

Why do I need keys in lists?

Keys let React identify items across renders. Without them, React diffs by position — if you prepend an item, every other item looks like it changed because it shifted index. With keys, React knows which item is which and only mutates what actually changed.

Use stable, unique IDs. Don't use array index unless the list is static and never reorders.

02Hooks

How does useState work?

useState returns a pair: the current state and a setter. React stores state per-component-per-call-position, which is why hooks must be called in the same order every render — that's how React knows which state is which.

The setter can take a value or a function:

setCount(count + 1);          // works, but...
setCount(c => c + 1);         // safer with concurrent updates

The functional form is necessary when the new state depends on the previous, especially in async contexts where the captured count may be stale.

How does useEffect work, and what's the most common mistake?

useEffect schedules a side effect to run after the render commits. The dependency array tells React when to re-run: if any dep changed by reference, the cleanup from the previous run fires, then the effect runs again.

useEffect(() => {
  const id = setInterval(() => fetchData(), 1000);
  return () => clearInterval(id); // cleanup
}, [fetchData]);

Most common mistake — stale closures. The effect captures values from the render it was created in. If your dep array doesn't list a value you're using, you'll keep reading old values forever.

// BUG: stale `count`
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, []); // missing `count`

Solutions: include the dep, use a ref, or use the functional updater form. The eslint react-hooks/exhaustive-deps rule catches most of this.

useMemo vs useCallback — when to use each?

useMemo caches a computed value. useCallback caches a function reference. Both prevent recomputation when deps haven't changed.

const filtered = useMemo(
  () => items.filter(predicate),
  [items, predicate]
);

const handleClick = useCallback(
  () => doThing(id),
  [id]
);

Use them when:

  • The computation is genuinely expensive (filtering 10k items, complex transforms)
  • You're passing the value/function to a memoized child and need referential stability
  • The value/function is a dep of another hook

Don't use them by default. They have a cost — the cache itself, the dep comparisons. For most cheap computations and handlers, just letting React re-create them is faster than memoizing.

What's useRef for?

Two main uses:

  1. Hold a mutable value across renders without triggering a re-render when it changes. Useful for IDs, previous values, "did mount" flags.
  2. Reference a DOM element for direct manipulation — focus, scroll, measuring, integrating with non-React libraries.
const inputRef = useRef(null);
useEffect(() => inputRef.current?.focus(), []);

return <input ref={inputRef} />;
What are the rules of hooks? Why?

Two rules:

  1. Call hooks at the top level. Not inside conditionals, loops, or nested functions.
  2. Call hooks only from React functions or other hooks.

Why: React tracks hooks by call order, not by name. If you conditionally skip a hook, all subsequent hook calls shift, and React reads the wrong state. The eslint plugin enforces this.

How do you build a custom hook?

A custom hook is just a function that:

  • Starts with use (so the linter knows)
  • Calls other hooks inside
  • Returns whatever shape you want
function useDebouncedValue(value, delay) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

Custom hooks are the right primitive for sharing stateful logic between components — they replace the old render-prop and HOC patterns for most cases.

03State management

When do you reach for global state vs local?

Default to local. Lift state up only when two components actually need to share it. Move to global state only when:

  • The state is genuinely cross-cutting (auth, theme, current user)
  • The prop drilling has gotten painful enough to be a bug magnet
  • You need to access the state from many disconnected places

Premature global state is one of the most common scaling problems I see. Once it's global, everything is implicitly coupled to it.

Context vs Redux vs Zustand vs Jotai — when do you pick what?

Context: small, infrequently-changing global state. Auth, theme, locale. Don't put rapidly-changing state in Context — every consumer re-renders on any change.

Redux (Toolkit): large apps with complex state transitions, time-travel debugging needs, enforced patterns. Heavier, more ceremony.

Zustand: lightweight, hook-based, no boilerplate. Most modern apps that don't need Redux's ecosystem land here.

Jotai / Recoil: atom-based, fine-grained reactivity. Good when you have lots of small independent pieces of state.

TanStack Query: not really competing with these — it handles server state. Pair with one of the above for client state.

Server state vs client state?

Different problems, different tools.

Client state — UI state. Modal open/closed, form input values, selected tab. Local to the user, lives in your app.

Server state — data fetched from a backend. Async, cached, can become stale, can fail, needs invalidation. The server owns the truth.

Conflating them was the original sin of Redux apps everywhere — putting fetched data in a Redux store and writing fetch/cache/loading/error logic by hand. Tools like TanStack Query exist to handle server state correctly so you can keep client state simple.

04Patterns & components

Controlled vs uncontrolled components?

Controlled: form state lives in React. The input value comes from state, changes go through onChange. React is the source of truth.

<input value={name} onChange={e => setName(e.target.value)} />

Uncontrolled: the DOM owns the state. You read it via a ref when you need it.

<input ref={inputRef} defaultValue="" />

Controlled is the React-idiomatic default. Uncontrolled is useful for: integrating with non-React form libraries, very large forms where re-rendering on every keystroke hurts perf, and file inputs (which are always uncontrolled).

What are render props and HOCs? Do you still use them?

Both are pre-hooks patterns for sharing logic.

  • HOC (Higher-Order Component): a function that takes a component and returns a new component with extra behavior. withAuth(MyComponent).
  • Render prop: a component takes a function as a child or prop and calls it to render — sharing data via the call.

Hooks largely replaced both. They're cleaner, easier to compose, easier to type. I'd still use HOCs for cross-cutting concerns at the rendering layer (e.g., error boundaries, lazy loading wrappers), but I wouldn't write a new HOC where a hook would do.

Error boundaries — what are they and where do you put them?

Class components with componentDidCatch or getDerivedStateFromError that catch errors thrown in their subtree during render, lifecycle, and constructors. They don't catch errors in event handlers, async code, or server-side rendering.

Where to put them: at least one at the app root for catastrophic failures. Then strategically around features that can fail independently — a chart panel, a data grid, a third-party widget — so one broken feature doesn't crash the page.

What's React.memo? When do you use it?

A higher-order component that does a shallow props comparison. If props haven't changed, it skips re-rendering.

const Row = React.memo(function Row({ item }) {
  return <div>{item.name}</div>;
});

Use it when:

  • The component is genuinely expensive to render
  • It re-renders often with the same props (typical in big lists)

Gotcha: Memo only helps if props are referentially stable. If you pass a new object/function every render from the parent, memo can't help. That's where useMemo/useCallback in the parent come in.

05Suspense & concurrent features

What is Suspense?

A React mechanism for declaratively handling async work — code splitting, data fetching — by letting components "suspend" while they wait, and showing fallback UI in the meantime.

<Suspense fallback={<Spinner />}>
  <LazyComponent />
</Suspense>

Originally used for React.lazy() code-split components. Modern data libraries (Relay, TanStack Query in some configs) integrate with Suspense for data fetching too.

What's concurrent rendering? useTransition?

Concurrent rendering lets React work on multiple state updates at once and prioritize them. Urgent updates (typing in an input) interrupt non-urgent ones (filtering a big list).

useTransition marks a state update as non-urgent — React keeps the UI responsive and lets the slow update happen in the background.

const [isPending, startTransition] = useTransition();
const handleType = (e) => {
  setQuery(e.target.value);  // urgent — input stays snappy
  startTransition(() => setResults(filter(items, e.target.value))); // can be deferred
};

This is genuinely useful for ERP-style data UIs where filter/search across large datasets can block the input.

06Common patterns to know

Compound components?

A pattern where multiple components work together as one cohesive unit, sharing state implicitly via context.

<Tabs defaultValue="overview">
  <Tabs.List>
    <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
    <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Panel value="overview">...</Tabs.Panel>
</Tabs>

Used by Radix, headless UI libraries. Great API for composable design system components.

Portals — when to use them?

createPortal(child, container) renders a React subtree into a different DOM node. Use cases: modals, tooltips, dropdowns — anywhere you need to escape a parent's overflow: hidden or stacking context. Events still propagate through the React tree as if the portal child was where it's defined, which is usually what you want.