Senior Frontend Interview Guide, Part 2: React 18 Fundamentals and Hooks
React is the center of gravity for most enterprise frontend roles, and React 18 changed the foundation under it. This chapter covers the core model that every React interview tests: components, JSX, props and state, rendering and reconciliation, and the complete set of built-in hooks. The next…

This article is Part 2 of 18 in The Senior Frontend Engineer Interview Guide, and includes 28 interview questions with model answers.
React is the center of gravity for most enterprise frontend roles, and React 18 changed the foundation under it. This chapter covers the core model that every React interview tests: components, JSX, props and state, rendering and reconciliation, and the complete set of built-in hooks. The next chapter builds on it with concurrency, Suspense, and performance.
The Mental Model: UI as a Function of State
React’s central idea is that a component is a function that takes inputs (props and state) and returns a description of the UI. You never tell React how to change the DOM; you describe what the UI should look like for the current data, and React works out the minimal set of DOM operations to get there.
function Balance({ amountCents, currency }) {
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency', currency,
}).format(amountCents / 100);
return <p className="balance">Available: {formatted}</p>;
}
Three properties follow from this model and come up again and again in interviews:
- Rendering must be pure. Given the same props and state, a component should return the same output and must not cause side effects (no network calls, no subscriptions, no mutation of outside variables) during rendering. Side effects belong in event handlers or effects.
- Rendering is not the same as updating the DOM. React may call your component many times. In React 18’s concurrent mode it may even start a render, throw it away, and start again. Only the commit phase touches the DOM.
- Data flows down. Parents pass props to children. Children communicate upward by calling functions passed down as props. Shared state is “lifted” to the closest common ancestor or placed in a store.
JSX
JSX is syntax sugar for creating elements. With the modern JSX transform (React 17+), the compiler turns JSX into calls to jsx() from react/jsx-runtime, so you no longer need import React in every file.
<Button variant="primary" onClick={save}>Save</Button>
// compiles to roughly:
jsx(Button, { variant: 'primary', onClick: save, children: 'Save' });
Rules of JSX worth knowing:
- An expression must return a single root; use fragments (
<>...</>) to group without extra DOM. - Attributes use camelCase (
className,htmlFor,onClick,tabIndex), exceptaria-*anddata-*. - Curly braces embed any JavaScript expression, not statements. Use ternaries,
&&, or helper functions for conditional output. false,null,undefined, andtruerender nothing. Numbers, including0, render as text.- JSX escapes embedded strings, which protects against most XSS. The escape hatch,
dangerouslySetInnerHTML, must only be used with sanitized content.
Components and Props
A component is a function whose name starts with a capital letter. Props are read-only; a component must never modify them.
function AccountCard({ account, onSelect, isSelected = false, children }) {
return (
<article
className={isSelected ? 'card card--selected' : 'card'}
onClick={() => onSelect(account.id)}
>
<h3>{account.nickname}</h3>
{children}
</article>
);
}
Useful prop patterns:
childrenfor composition: the parent decides what goes inside.- Render props (a prop that is a function returning JSX) for sharing behavior with flexible rendering. Mostly replaced by hooks, but still used in libraries such as table and virtualization components.
- Spreading rest props onto a native element (
<button {...rest}>) so consumers can passaria-*,data-*, and event handlers. Be careful not to spread unknown props onto DOM elements.
Class Components
You will mostly write function components, but enterprise code bases still contain class components, and interviewers sometimes ask about their lifecycle. The mapping is:
| Class lifecycle | Hook equivalent |
|---|---|
constructor (initial state) |
useState(initial) or lazy initializer |
componentDidMount |
useEffect(() => {...}, []) |
componentDidUpdate |
useEffect(() => {...}, [deps]) |
componentWillUnmount |
cleanup function returned from useEffect |
shouldComponentUpdate |
React.memo with a custom comparison |
getDerivedStateFromProps |
compute during render, or update state during render with a guard |
getSnapshotBeforeUpdate |
useLayoutEffect (approximately) |
componentDidCatch / getDerivedStateFromError |
No hook; error boundaries must still be classes |
Error boundaries are the one feature that still requires a class component (or a library like react-error-boundary).
Rendering, Reconciliation, and Keys
When state changes, React re-renders that component and, by default, all of its descendants. It then compares the new element tree with the previous one (reconciliation) and applies the differences to the DOM.
The diffing algorithm uses two heuristics that make it O(n) instead of O(n³):
- Elements of different types produce different trees. If a
<div>becomes a<section>, or<LoginForm>becomes<SignupForm>, React destroys the old subtree (including its state) and builds a new one. - Children in a list are matched by their
keyprop.
Keys
Keys tell React which item in a list is which across renders. They must be stable, unique among siblings, and derived from the data (such as a database ID).
<ul>
{transactions.map(tx => <TransactionRow key={tx.id} tx={tx} />)}
</ul>
Using the array index as a key is acceptable only when the list is static and never reordered, filtered, or prepended to. Otherwise, when an item is inserted at the top, every index shifts, React thinks every row changed, and any local state (an open menu, a text input’s value) moves to the wrong row.
Keys are also a tool for resetting state deliberately. Rendering <PaymentForm key={payeeId} /> gives you a fresh form, with fresh state, whenever the payee changes.
Render and Commit Phases
React’s work is split into two phases:
- Render phase: call components, compute the new tree, diff it. Pure and, in concurrent React, interruptible.
- Commit phase: apply DOM mutations, then run layout effects (
useLayoutEffect) synchronously, then let the browser paint, then run passive effects (useEffect).
This split is why reading the DOM size belongs in useLayoutEffect (before paint, so no flicker) while data fetching and subscriptions belong in useEffect (after paint, so they do not block it).
React 18: What Changed
React 18 shipped in March 2022 and introduced the concurrent renderer. The headline changes, each of which can be an interview question:
createRoot.ReactDOM.renderis replaced withcreateRoot(container).render(<App />). Only roots created this way get concurrent features.- Automatic batching. State updates are batched everywhere: inside promises, timeouts, and native event handlers, not only inside React event handlers. Several
setStatecalls in afetch().then()cause one render instead of several.flushSyncopts out when you truly need a synchronous update. - Transitions.
startTransitionanduseTransitionmark updates as non-urgent so urgent updates (typing, clicking) stay responsive. useDeferredValue. Defers re-rendering of a non-urgent part of the UI.- Suspense on the server. Streaming server rendering with
renderToPipeableStreamand selective hydration. - New hooks.
useId,useSyncExternalStore,useInsertionEffect. - Strict Mode double-invocation of effects. In development, React mounts, unmounts, and re-mounts every component once to surface effects that do not clean up properly.
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
State with useState
const [filters, setFilters] = useState({ status: 'all', query: '' });
Essential details:
- State is a snapshot. Inside one render,
filtersnever changes. CallingsetFiltersschedules a re-render with the new value; it does not mutate the variable you are holding. - Functional updates use the latest queued value, which matters when the next state depends on the previous one or when updates happen in a stale closure:
setCount(c => c + 1). - Lazy initialization avoids recomputing an expensive initial value on every render:
useState(() => loadDraftFromStorage()). - Bail-out: if the new value is
Object.isequal to the current one, React skips re-rendering the children. - Immutability. Never mutate state objects in place. Create new objects and arrays so that reference comparison detects the change.
// Wrong: mutates, React may not re-render
filters.status = 'pending';
setFilters(filters);
// Right
setFilters(prev => ({ ...prev, status: 'pending' }));
Deciding What Belongs in State
The best state is the least state. Before adding a useState, ask:
- Can it be computed from props or other state during render? Then compute it (
const total = items.reduce(...)), possibly withuseMemoif expensive. - Does it stay the same across renders and not affect output? Use a
useRef. - Is it shared by several components? Lift it up or move it to Context or a store.
- Is it server data? Consider a server-state library rather than hand-rolled state (see Chapter 7).
Duplicated state (for example, storing both items and selectedItem object) gets out of sync. Store the selectedId and derive the item.
useReducer
useReducer centralizes complex state transitions in a pure function.
function transferReducer(state, action) {
switch (action.type) {
case 'field':
return { ...state, values: { ...state.values, [action.name]: action.value } };
case 'submit':
return { ...state, status: 'submitting', error: null };
case 'success':
return { ...state, status: 'done', confirmation: action.confirmation };
case 'failure':
return { ...state, status: 'error', error: action.error };
default:
throw new Error(`Unknown action ${action.type}`);
}
}
const [state, dispatch] = useReducer(transferReducer, initialState);
Choose useReducer over useState when the next state depends on the previous one in non-trivial ways, when several values change together, or when you want to test transitions in isolation. dispatch is stable across renders, so it is safe to pass down without memoization.
Effects with useEffect
Effects synchronize a component with something outside React: the network, a subscription, a timer, the document title, a non-React widget.
useEffect(() => {
const controller = new AbortController();
setStatus('loading');
api.get(`/accounts/${accountId}/transactions`, { signal: controller.signal })
.then(res => { setRows(res.data); setStatus('success'); })
.catch(err => { if (!controller.signal.aborted) setStatus('error'); });
return () => controller.abort();
}, [accountId]);
The rules:
- The effect runs after the commit and after the browser paints.
- The dependency array lists every reactive value (props, state, values derived from them, functions defined in the component) that the effect reads. The
react-hooks/exhaustive-depslint rule enforces this; do not silence it. - The cleanup runs before the effect re-runs and when the component unmounts.
- No array: runs after every render. Empty array: runs after mount only (but twice in development Strict Mode).
You Might Not Need an Effect
Many effects in real code bases are unnecessary and cause extra renders or bugs. Common cases:
| Instead of an effect that… | Do this |
|---|---|
| Computes derived data from props/state | Compute during render |
| Resets state when a prop changes | Pass a key to reset the component |
| Responds to a user event (submit, click) | Put the logic in the event handler |
| Notifies a parent of a state change | Call the parent’s callback in the same handler |
| Fetches data in many components | Use a data-fetching library or framework loader |
| Subscribes to an external store | Use useSyncExternalStore |
Why Effects Run Twice in Development
In Strict Mode, React 18 simulates unmounting and re-mounting each component immediately after it mounts. If an effect has no cleanup (opens a socket without closing it, adds a listener without removing it), you will see duplicated behavior. The fix is never to disable Strict Mode but to write a proper cleanup. This prepares components for features such as Offscreen/Activity rendering, where React keeps state while hiding and re-showing UI.
Race Conditions
If accountId changes from A to B quickly, the response for A may arrive after the response for B and overwrite it. Aborting the request in the cleanup, or tracking an ignore flag, solves this:
useEffect(() => {
let ignore = false;
fetchStatement(accountId).then(data => { if (!ignore) setStatement(data); });
return () => { ignore = true; };
}, [accountId]);
useLayoutEffect and useInsertionEffect
useLayoutEffect has the same signature as useEffect but runs synchronously after DOM mutations and before the browser paints. Use it to measure layout and synchronously re-position things (tooltips, popovers) to avoid a visible flicker. It blocks painting, so keep it short. It does not run during server rendering.
useInsertionEffect runs before any layout effects and is intended only for CSS-in-JS libraries that need to inject <style> tags before layout is read.
Refs: useRef and forwardRef
useRef returns a mutable object { current } that persists for the component’s lifetime. Changing ref.current does not trigger a re-render.
Two uses:
- Accessing DOM nodes: focus management, measuring, scrolling, integrating non-React libraries.
- Storing mutable values that should not cause renders: timer IDs, the previous value of a prop, an
AbortController, a flag for “is mounted”.
function SearchBox() {
const inputRef = useRef(null);
useEffect(() => { inputRef.current?.focus(); }, []);
return <input ref={inputRef} type="search" aria-label="Search transactions" />;
}
Do not read or write ref.current during rendering (except lazy initialization), because rendering must be pure.
In React 18, function components do not receive ref as a prop; you wrap them in forwardRef to expose a DOM node to a parent. (React 19 makes ref a regular prop, but React 18 code still needs forwardRef.)
const TextField = React.forwardRef(function TextField({ label, ...props }, ref) {
const id = useId();
return (
<div className="field">
<label htmlFor={id}>{label}</label>
<input id={id} ref={ref} {...props} />
</div>
);
});
useImperativeHandle customizes what the parent receives through the ref, for example exposing only focus() and clear() instead of the whole DOM node.
Memoization: useMemo, useCallback, and React.memo
useMemo(fn, deps)caches the result of a computation between renders.useCallback(fn, deps)caches a function identity; it isuseMemo(() => fn, deps).React.memo(Component)skips re-rendering a component when its props are shallowly equal to the previous props.
They work together. React.memo on a child is useless if the parent passes a new inline function or object on every render, because the shallow comparison fails. useCallback and useMemo keep those references stable.
const TransactionTable = React.memo(function TransactionTable({ rows, onSelect }) {
/* expensive render of 500 rows */
});
function Statement({ transactions, query }) {
const rows = useMemo(
() => transactions.filter(t => t.description.includes(query)),
[transactions, query]
);
const handleSelect = useCallback(id => setSelectedId(id), []);
return <TransactionTable rows={rows} onSelect={handleSelect} />;
}
When not to memoize: cheap computations, components that almost always receive new props anyway, and as a substitute for fixing an architectural problem (such as state placed too high in the tree). Memoization has a cost: memory, comparison work, and readability. Measure with the React Profiler first. (React Compiler, introduced alongside React 19, automates much of this, but in React 18 code bases it is manual.)
Context
Context passes a value through the tree without prop drilling.
const ThemeContext = React.createContext('light');
function App() {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
<Layout />
</ThemeContext.Provider>
);
}
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}
Every consumer re-renders when the provider’s value changes by reference. Memoize the value object and split unrelated concerns into separate contexts. Chapter 7 compares Context with Zustand in depth.
Custom Hooks
A custom hook is a function whose name starts with use and that calls other hooks. It shares stateful logic, not state: each component that calls the hook gets its own independent state.
function useDebouncedValue(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
function useMediaQuery(query) {
const subscribe = useCallback(cb => {
const mql = window.matchMedia(query);
mql.addEventListener('change', cb);
return () => mql.removeEventListener('change', cb);
}, [query]);
return useSyncExternalStore(
subscribe,
() => window.matchMedia(query).matches,
() => false // server snapshot
);
}
Good custom hooks have a clear, single purpose, a stable API, handle cleanup, and are tested independently with renderHook.
Rules of Hooks
- Only call hooks at the top level of a component or custom hook: never inside conditions, loops, nested functions, or after an early return.
- Only call hooks from React function components or custom hooks.
The reason is implementation: React identifies each hook by its call order within the component. If the order changes between renders, state gets attached to the wrong hook. The eslint-plugin-react-hooks plugin enforces both rules.
useId
useId generates a unique, stable ID that matches between server and client rendering, which solves hydration mismatches caused by random or counter-based IDs. Use it to connect labels, inputs, and ARIA attributes. Do not use it for list keys.
useSyncExternalStore
This hook is the correct way to subscribe to a store that lives outside React (a Redux or Zustand store, browser APIs such as navigator.onLine, a WebSocket cache). It guarantees that all components see a consistent snapshot during a concurrent render, avoiding “tearing” where different parts of the UI show different versions of the same data.
function useOnlineStatus() {
return useSyncExternalStore(
cb => {
window.addEventListener('online', cb);
window.addEventListener('offline', cb);
return () => {
window.removeEventListener('online', cb);
window.removeEventListener('offline', cb);
};
},
() => navigator.onLine,
() => true
);
}
The getSnapshot function must return the same value (by reference) if nothing changed; returning a new object each time causes an infinite loop.
Events in React
React wraps native events in SyntheticEvent objects with a consistent cross-browser API. Since React 17, listeners are attached to the root container rather than document, which makes it possible to run multiple React versions on one page (relevant to micro frontends). Event pooling was removed in React 17, so you can access event asynchronously.
Two subtle points: onChange on inputs fires on every keystroke (it behaves like the native input event), and onFocus/onBlur bubble in React (they use focusin/focusout under the hood).
Forms: Controlled and Uncontrolled
A controlled input’s value lives in React state and every change goes through onChange. An uncontrolled input keeps its own value in the DOM, read through a ref or FormData on submit.
// Controlled
<input value={amount} onChange={e => setAmount(e.target.value)} />
// Uncontrolled
<form onSubmit={e => {
e.preventDefault();
const data = new FormData(e.currentTarget);
submit(Object.fromEntries(data));
}}>
<input name="amount" defaultValue="0.00" />
</form>
Controlled inputs enable instant validation, formatting, and conditional UI; uncontrolled inputs are simpler and faster for large forms. Libraries like React Hook Form use uncontrolled inputs with refs for performance and offer schema validation with Zod or Yup. Switching an input from uncontrolled (value={undefined}) to controlled triggers a warning; initialize state to '' rather than undefined.
Portals
createPortal(children, domNode) renders children into a different DOM node while keeping them in the same React tree, so context and event bubbling work as if they were rendered in place. Use portals for modals, toasts, and tooltips that must escape a parent’s overflow: hidden or stacking context.
Error Boundaries
An error boundary catches errors thrown during rendering, in lifecycle methods, and in constructors of its descendants, and renders a fallback UI.
class ErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) { return { error }; }
componentDidCatch(error, info) { logger.error(error, info.componentStack); }
render() {
if (this.state.error) return this.props.fallback ?? <p>Something went wrong.</p>;
return this.props.children;
}
}
Boundaries do not catch errors in event handlers, asynchronous code (timeouts, promises), server rendering, or the boundary itself. Handle those with try/catch and surface them through state. Place boundaries at route level, around independently failing widgets, and around each remote micro frontend so one team’s bug cannot blank the whole page.
Component Design Patterns
- Container and presentational split. Logic (data fetching, state) in hooks or container components; pure rendering in presentational components. Hooks have made this split lighter but the principle remains.
- Compound components. A parent shares implicit state with its children through Context:
<Tabs><Tabs.List/><Tabs.Panel/></Tabs>. Covered in Chapter 6. - Controlled versus uncontrolled components. Allow both by accepting
value/onChangeand falling back to internal state when they are absent. - Higher-order components (HOCs). Functions that take a component and return an enhanced component (
withAuth(Page)). Largely replaced by hooks, but still seen around routing and analytics.
Interview Questions and Answers
Q1. What is the virtual DOM, and is it why React is fast?
The virtual DOM is the tree of plain JavaScript objects (React elements) that components return. React diffs the new tree against the previous one and applies minimal DOM updates. It is not inherently faster than hand-written DOM code; its value is that it lets you write declarative UI while React handles efficient updates, batching, and scheduling. Performance comes from avoiding unnecessary work, which still depends on good component design.
Q2. What happens when you call setState?
React enqueues an update on that component’s fiber and schedules a render with a priority based on where the update came from (a click is urgent; a transition is not). In React 18 updates are batched automatically. During the render, React calls the component with the new state, reconciles the returned tree with the previous one, and in the commit phase applies DOM changes and runs effects. If the new state is Object.is equal to the old state, React may bail out.
Q3. What is automatic batching in React 18?
React groups multiple state updates into a single re-render even when they happen inside promises, timeouts, or native event handlers. Previously, batching only happened inside React event handlers. This reduces renders without code changes. flushSync forces an immediate synchronous render when needed, for example before measuring the DOM.
Q4. Why are keys important, and why not use the index?
Keys let React match list items between renders so it can preserve their state and DOM nodes and move them efficiently. Indexes break when items are inserted, removed, or reordered: the key that used to belong to one item now belongs to another, so state and focus attach to the wrong row and performance suffers. Use a stable ID from the data.
Q5. Explain the difference between useEffect and useLayoutEffect.
Both run after React updates the DOM. useLayoutEffect runs synchronously before the browser paints, so it can measure layout and make changes without a visible flicker, but it blocks paint. useEffect runs after paint and does not block it, so it is the default for data fetching, subscriptions, and logging.
Q6. Why does my effect run twice in development?
React 18 Strict Mode intentionally mounts, unmounts, and re-mounts components in development to verify that effects clean up correctly. It does not happen in production. If double execution causes a bug (two subscriptions, two analytics events), the effect is missing a cleanup or the logic belongs in an event handler.
Q7. What is a stale closure in hooks, and how do you avoid it?
A function created in one render captures that render’s props and state. If it is called later without being recreated (in a setInterval with an empty dependency array, or a memoized callback with missing dependencies), it reads old values. Avoid it by listing all dependencies, using functional state updates, or storing the latest value in a ref when you truly need a stable callback that reads fresh data.
Q8. When should you use useMemo and useCallback?
When a computation is measurably expensive, or when you need referential stability: passing callbacks or objects to React.memo children, or using them as dependencies of other hooks. Do not wrap everything by default; the comparison and memory costs can outweigh the savings, and it adds noise. Profile first.
Q9. What does React.memo do, and what are its pitfalls?
It memoizes a component so it re-renders only when its props change by shallow comparison. Pitfalls: inline objects, arrays, and functions create new references every render and defeat it; children passed as JSX are new elements every render; and context changes still re-render the component regardless of memo.
Q10. How do you share logic between components?
With custom hooks, which encapsulate stateful logic and can be composed. Each consumer gets independent state. For shared state rather than logic, lift state up, use Context, or use a store such as Zustand. Older patterns are HOCs and render props.
Q11. What are the rules of hooks, and why do they exist?
Call hooks only at the top level and only from components or custom hooks. React tracks hooks by call order in a list attached to each component’s fiber; conditional calls would change the order between renders and attach state to the wrong hook.
Q12. Controlled versus uncontrolled components?
Controlled components keep form values in React state and update via onChange, giving full control for validation and formatting at the cost of a render per keystroke. Uncontrolled components let the DOM hold the value and read it via refs or FormData, which is simpler and faster for large forms. Many form libraries use uncontrolled inputs internally.
Q13. How does React handle events?
React attaches a small number of listeners at the root container and dispatches synthetic events to components (event delegation). Synthetic events normalize browser differences. Since React 17, the root is the container rather than document, which allows multiple React roots and versions to coexist, and event pooling no longer exists.
Q14. What are error boundaries, and what don’t they catch?
Class components implementing getDerivedStateFromError and/or componentDidCatch that render a fallback when a descendant throws during render or lifecycle. They do not catch errors in event handlers, asynchronous callbacks, server rendering, or the boundary itself.
Q15. How would you fetch data in a component correctly?
Start the request in an effect keyed on the inputs, track loading and error states, abort or ignore stale responses in the cleanup to avoid race conditions, and avoid waterfalls by fetching in parallel or at the route level. In production I prefer TanStack Query or a framework loader, which add caching, de-duplication, retries, and background refetching.
Q16. What is useId for?
Generating unique IDs that are stable across server and client renders, so that htmlFor/id and aria-describedby relationships do not cause hydration mismatches. It should not be used for list keys.
Q17. What problem does useSyncExternalStore solve?
Tearing in concurrent rendering. When React can pause a render, an external store might change midway, so components rendered earlier and later see different values. useSyncExternalStore forces a consistent snapshot and is the supported way for libraries like Redux and Zustand to integrate with React 18.
Q18. Explain lifting state up versus prop drilling.
Lifting state moves it to the closest common ancestor so siblings can share it. When the common ancestor is far away, passing props through many intermediate components (prop drilling) becomes noisy and brittle. Solutions include composition (passing components as children so intermediate layers do not need the data), Context, or a store.
Q19. How do you reset a component’s state when a prop changes?
Give the component a key derived from that prop. When the key changes, React unmounts the old instance and mounts a new one with fresh state. This is cleaner than an effect that watches the prop and resets state, which causes an extra render with stale values.
Q20. What are portals used for?
Rendering content into a DOM node outside the parent hierarchy, such as a modal root, while keeping React context and event propagation intact. They solve clipping and stacking issues from overflow: hidden and z-index contexts.
Q21. What is forwardRef, and when do you need it?
In React 18, function components cannot receive a ref through props. forwardRef passes the ref through so a parent can access the underlying DOM node, which reusable inputs and buttons need for focus management and form libraries. useImperativeHandle restricts what is exposed.
Q22. What is the difference between state and refs?
State changes trigger a re-render and are a snapshot per render. Ref changes do not trigger renders and are mutable across renders. Use state for values that affect output; use refs for DOM nodes and mutable values like timer IDs.
Q23. How does React decide whether to re-render a child?
By default, every child re-renders when its parent re-renders, regardless of props. A child skips rendering only if it is wrapped in React.memo and its props are shallowly equal, or if the element object itself is identical to last time (as happens with children passed from above). Context consumers re-render when the context value changes.
Q24. What is derived state, and why is it often a bug?
Derived state is state copied or computed from props or other state. Storing it separately means it can go out of sync when the source changes. Compute it during render instead, memoizing if expensive, or reset with a key.
Q25. How do you avoid unnecessary re-renders in a large form?
Keep field state local to field components or use an uncontrolled form library, split context so unrelated fields do not subscribe to the whole form, memoize heavy subcomponents, and debounce validation. Measure with the React Profiler’s “why did this render” information.
Q26. What is the difference between createRoot and ReactDOM.render?
createRoot creates a concurrent root that enables React 18 features: automatic batching everywhere, transitions, Suspense improvements, and streaming hydration with hydrateRoot. ReactDOM.render creates a legacy root that behaves like React 17 and is deprecated.
Q27. When is it appropriate to use dangerouslySetInnerHTML?
Only when rendering trusted or sanitized HTML, such as CMS content passed through DOMPurify with a strict allowlist. Never with raw user input. The awkward name exists to make the XSS risk obvious in code review.
Q28. How do you structure a large React application?
By feature (domain) rather than by technical type: each feature folder contains its components, hooks, API calls, tests, and styles, with a shared design system and utilities layer. Enforce boundaries with lint rules, keep server state in a query library and UI state local, and split routes into lazy-loaded chunks. At larger scale, split into micro frontends owned by separate teams.
Key Takeaways
- Components are pure functions of props and state; side effects live in handlers and effects.
- React 18 introduced concurrent rendering, automatic batching, transitions, and new hooks.
- State is an immutable snapshot per render; derive what you can and store the minimum.
- Effects synchronize with external systems and must clean up; many effects are unnecessary.
- Memoization is a targeted optimization, not a default.
- Keys, error boundaries, portals, and refs each solve a specific problem worth explaining precisely.
← Part 1: Modern JavaScript (ES6+) · All chapters · Part 3: Advanced React 18: Concurrency, Suspense, and Performance →







