React Technical Interview Topics
Core React concepts for technical interviews. Compact read-and-recall short form info.
What is React & the Virtual DOM
What it is: React is a UI library (not a framework) by Meta for building component-based user interfaces. It manages DOM updates efficiently through a virtual DOM diffing algorithm.
- Virtual DOM: an in-memory representation of the real DOM. On state change, React computes the diff (reconciliation) and applies only the minimum real DOM mutations
- React is un-opinionated about routing, state management, and HTTP; you choose the libraries
- Renders to the browser DOM via
react-dom, to native views via React Native, or to strings viarenderToString(SSR)
Note: The virtual DOM is not inherently faster than direct DOM manipulation; it is a predictability/developer-experience trade-off that happens to be fast enough for most UIs.
JSX
What it is: A syntax extension that lets you write HTML-like markup inside JavaScript. Babel/SWC compiles it to React.createElement() calls.
- JSX is not HTML: use
classNamenotclass,htmlFornotfor, camelCase event names (onClick) - Expressions inside
{}:{user.name},{isLoggedIn && <Dashboard />},{items.map(...)} - A component must return a single root element; wrap siblings in
<>...</>(Fragment) to avoid extra DOM nodes - Self-closing tags are required:
<img />,<br />
const Greeting = ({ name }: { name: string }) => (
<p className="greeting">Hello, {name}!</p>
);
Note:
React.createElementis still what runs at runtime. Understanding JSX as syntactic sugar helps debug cryptic Babel/TypeScript errors.
Class vs Functional Components
What it is: Two ways to define React components. Functional components with hooks are the modern standard.
| Class Component | Functional Component | |
|---|---|---|
| State | this.state / setState |
useState |
| Lifecycle | componentDidMount, etc. |
useEffect |
| Boilerplate | More (extend, this) |
Less |
| Performance | Slightly heavier | Lighter |
| Hooks | Not supported | Supported |
- Class components still work; no plans to remove them
thisbinding issues in class components are a common bug source
Note: Prefer functional components for all new code. Hooks give full access to React features with less ceremony and no
thisconfusion.
State & Props
What it is: The two data mechanisms in React.
- Props: data passed from parent to child. Read-only in the receiving component. Changing a prop requires the parent to update its own state.
- State: data local to a component. Changing state triggers a re-render of that component and its children.
- State updates are asynchronous and batched. Never read state immediately after
setState/useStatesetter.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Note: Use the functional updater form
setCount(c => c + 1)when new state depends on previous state; avoids stale closure bugs in event handlers.
Hooks: useState & useEffect
What it is: Functions that let functional components tap into React features (state, lifecycle, context, etc.).
useState:
- Returns [value, setter]; setter triggers re-render
- State is preserved across renders; initialized only once
useEffect:
- Runs side effects after render (data fetching, subscriptions, DOM mutations)
- useEffect(fn): runs after every render
- useEffect(fn, [dep]): runs when dep changes
- useEffect(fn, []): runs once, mount equivalent
- Return a cleanup function to cancel subscriptions/timers on unmount
useEffect(() => {
const sub = subscribe(id);
return () => sub.unsubscribe(); // cleanup
}, [id]);
Note: Including all values read inside
useEffectin the dependency array is required by theexhaustive-depsESLint rule. Missing deps cause stale closures.
Rules of Hooks
What it is: Constraints React enforces on hook usage to preserve hook call order across renders.
- Only call hooks at the top level: not inside loops, conditions, or nested functions
-
Only call hooks from React function components or custom hooks: not from plain JavaScript functions
-
React relies on call order to associate state with the correct hook across re-renders
- Violating rule 1 causes state to be assigned to the wrong hook on conditional renders
Note: The
eslint-plugin-react-hookspackage enforces both rules at build time. Enable it; the rules are easy to violate accidentally.
Props Drilling & Context API
What it is: Prop drilling passes data through intermediate components that don't need it. Context avoids this by broadcasting values to any descendant.
React.createContext(defaultValue)creates a context<Context.Provider value={...}>wraps the tree to provide the valueuseContext(MyContext)reads the value in any descendant, no intermediate wiring
const ThemeContext = createContext<'light' | 'dark'>('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<DeepChild />
</ThemeContext.Provider>
);
}
function DeepChild() {
const theme = useContext(ThemeContext);
return <div className={theme}>...</div>;
}
Note: Context re-renders all consumers when the value changes. For high-frequency updates (e.g., mouse position), context is not ideal; consider Zustand or Jotai instead.
Performance: memo, useMemo, useCallback
What it is: Tools to skip unnecessary re-renders and expensive recalculations.
React.memo(Component): wraps a component; skips re-render if props are shallowly equaluseMemo(() => compute(), [deps]): memoizes an expensive computed valueuseCallback(() => fn, [deps]): memoizes a function reference (useful when passing callbacks to memoized children)
const ExpensiveList = React.memo(({ items }: { items: Item[] }) => (
<ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>
));
const handleClick = useCallback(() => doSomething(id), [id]);
Note: Don't memoize everything by default;
memoanduseMemohave a cost too. Profile first. Only memoize when a measured re-render is actually slow.
Reconciliation
What it is: The algorithm React uses to diff the previous and next virtual DOM trees and decide the minimum set of real DOM changes.
- React compares trees top-down, element type first; if the type changes, the subtree is torn down and rebuilt
- For lists, React uses the
keyprop to match old and new items - Stable, unique keys (not array index) prevent unnecessary unmount/remount of list items
Note: Using array index as
keycauses bugs when items reorder; React matches by position, not identity => stale state in reordered items.
Custom Hooks
What it is: Functions that start with use and encapsulate reusable stateful logic by composing built-in hooks.
- Extract repeated
useState+useEffectpatterns into a custom hook - Return whatever the consumer needs (values, setters, callbacks)
- Each call to a custom hook has its own independent state
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
});
useEffect(() => localStorage.setItem(key, JSON.stringify(value)), [key, value]);
return [value, setValue] as const;
}
Note: Custom hooks are the modern replacement for HOCs and render props. They compose cleanly without adding wrapper nodes to the component tree.
Controlled vs Uncontrolled Components
What it is: Two ways to handle form input in React.
- Controlled: React state is the single source of truth.
valueandonChangeare both set. Every keystroke updates state. - Uncontrolled: the DOM manages its own state. Access values via
refon demand (e.g., on submit).
// Controlled
const [name, setName] = useState('');
<input value={name} onChange={e => setName(e.target.value)} />
// Uncontrolled
const ref = useRef<HTMLInputElement>(null);
<input ref={ref} defaultValue="initial" />
// read: ref.current?.value
Note: Controlled components are easier to validate and integrate with form libraries. Uncontrolled is simpler for one-off forms where you only need the value at submit time.
Error Boundaries
What it is: Class components that catch JavaScript errors anywhere in their child component tree and display a fallback UI instead of crashing.
- Must be class components; no hook equivalent exists yet
static getDerivedStateFromError(error): update state to show fallbackcomponentDidCatch(error, info): log the error- Does not catch async errors, event handler errors, or SSR errors
class ErrorBoundary extends React.Component<{ children: ReactNode }, { hasError: boolean }> {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
render() {
return this.state.hasError ? <h1>Something went wrong.</h1> : this.props.children;
}
}
Note: Wrap each major section of the UI in its own error boundary so one broken widget doesn't take down the whole page.
SSR & Suspense / Lazy Loading
What it is: Techniques for deferring component loading and rendering on the server.
React.lazy: dynamically imports a component; must be wrapped in<Suspense fallback={...}>- Suspense: shows a fallback while the lazy component (or data) loads; integrates with SSR in React 18+
- SSR with React:
renderToString/renderToPipeableStreamrenders the initial HTML on the server; client hydrates it - Next.js is the dominant React SSR framework (App Router uses React Server Components)
const Chart = React.lazy(() => import('./Chart'));
<Suspense fallback={<Spinner />}>
<Chart data={data} />
</Suspense>
Note:
React.lazyonly works with default exports. For named exports, re-export as default in a wrapper or use a dynamic import expression that selects the named export.
Testing: Jest & React Testing Library
What it is: The standard React testing stack: Jest as the test runner, RTL as the rendering/query layer.
- RTL philosophy: test behavior, not implementation. Query by accessible roles, labels, and text, not CSS classes or component internals.
render(<Component />): renders into a test DOMscreen.getByRole,screen.getByLabelText,screen.getByText: preferred queriesuserEventsimulates real user interactions (type, click, tab)waitFor/findBy*for async assertions
test('submits form', async () => {
render(<LoginForm onSubmit={mockFn} />);
await userEvent.type(screen.getByLabelText('Email'), 'a@b.com');
await userEvent.click(screen.getByRole('button', { name: /login/i }));
expect(mockFn).toHaveBeenCalledWith({ email: 'a@b.com' });
});
Note: Avoid
getByTestIdwhen an accessible query exists; tests that rely ondata-testidare brittle and don't verify accessibility at all.
Questions referenced from devinterview.io. Answers written independently.