Components, state, props and lifecycle
The questions here look like definitions and are graded like judgement. Anyone can say state is internal and props come from the parent; the separating answer is why that boundary exists and what breaks when you cross it - a child that writes to props, a component that reads state on the line after setting it, a list keyed by index. The other half of the topic is the JSX rules, which are not style advice: one root because a function returns one value, className because class is reserved, and the && trap that renders a bare 0 into your page.
The boundary that everything else hangs off
Props are the arguments a component was called with. State is the memory it keeps between calls. Data flows one way - down - and the only way a child affects a parent is by calling a function the parent handed it.
One-way flow is a constraint that buys you debuggability: when a value on screen is wrong, there is exactly one place it could have come from, and you walk up. In a two-way system any component that can see a value can also have written it, and the search is the whole app. That is the answer to "why are props read-only" - not because React forbids mutation for its own sake, but because the parent, and possibly several siblings, are reading the same object and none of them will be told you changed it.
- props
- Read-only. Owned by whoever rendered you. Changing them means asking the owner to change them, through a callback. Mutating props works in plain JavaScript and is a bug in React: React does not re-render on it, so the screen and the data disagree.
- state
- Owned by the component, private to that instance, and the only thing that makes React re-render when it changes. Two instances of the same component have entirely separate state.
- lifting state up
- When two siblings need the same value, it moves to their closest common parent and comes back down as props, with a setter passed down as a callback. This is the standard answer to "how do two components share state", before anyone says Context or Redux.
- derived values are not state
- If it can be computed from props or existing state during render, compute it during render. Storing it in state gives you two sources of truth and an effect that keeps them in sync, which is a whole class of bug for no gain.
The class lifecycle, and which parts are still asked
Mount runs constructor, then getDerivedStateFromProps, then render, then the DOM is committed, then componentDidMount. Update runs getDerivedStateFromProps, then shouldComponentUpdate, then render, then getSnapshotBeforeUpdate, then componentDidUpdate. Unmount runs componentWillUnmount. Errors thrown below run getDerivedStateFromError and componentDidCatch.
In practice four of those carry the whole interview: componentDidMount (subscribe, fetch, measure), componentDidUpdate (react to a prop change, guarded by comparing with prevProps or you loop), componentWillUnmount (clean up whatever you started), and shouldComponentUpdate (skip a render). The hooks mapping is worth having ready: useEffect with an empty array is roughly didMount plus willUnmount, useEffect with dependencies is roughly didUpdate, and React.memo is roughly shouldComponentUpdate.
The three componentWill* methods - componentWillMount, componentWillReceiveProps, componentWillUpdate - were deprecated and renamed with an UNSAFE_ prefix because they can run more than once for a single commit under interruptible rendering. If you name one as the answer to "where would you fetch data", you have dated yourself by about eight years.
Questions
Answer each one yourself before you open it.
01Function components or class components - what is the difference and which do you write?
Function components are functions that take props and return elements, with state and lifecycle supplied by hooks. Classes extend React.Component, hold state on this.state, and use lifecycle methods. Everything new is a function component.
The important part is that they are not two rendering models - React treats both the same way, and you can mix them in one tree. What changed is code reuse: with classes, sharing stateful logic meant HOCs or render props, which nest; with hooks it is a function call, which composes flat. Hooks also let you group code by what it does rather than by when it runs - one subscription's setup and teardown sit together instead of in two different methods a hundred lines apart.
Follow-up Is there anything a class can do that a function cannot?
One thing: be an error boundary. getDerivedStateFromError and componentDidCatch have no hook equivalent, so every codebase still has one class, or imports a library that has one.
02What is an error boundary?
A class component that catches JavaScript errors thrown anywhere in its child tree during rendering, in lifecycle methods, and in constructors - and renders a fallback UI instead of letting the whole tree unmount.
Without one, an error during render unmounts the entire React tree and the user gets a blank page - that is deliberate on React's part, on the grounds that a broken UI is worse than no UI. getDerivedStateFromError sets the state that renders the fallback; componentDidCatch is where you log. What it does not catch is the part people get wrong: errors inside event handlers, anything asynchronous like a setTimeout or a promise rejection, errors during server rendering, and errors thrown by the boundary itself.
Follow-up How do you catch an error in an event handler then?
An ordinary try/catch, and put the error in state if you want to show something. Handlers do not run during render, so React is not in a position to intercept them.
03Stateful versus stateless components?
Stateful holds state and re-renders itself when it changes; stateless renders purely from its props. The words predate hooks, when the split lined up with class versus function - it does not any more.
Worth saying the modern version: the useful distinction now is which component owns a piece of state, not what syntax it is written in. Keep the number of stateful components small and push them as low in the tree as the data allows.
04Presentational versus container components?
A presentational component decides how things look and takes everything through props; a container decides what data to show and passes it down. It was the standard way to keep data-fetching out of markup before hooks.
Be honest about its status: the author of the original article walked it back, because a custom hook does the same separation without adding a component to the tree. The idea survives - most components should not know where their data came from - but the two-component pattern is no longer the way to get it. Saying that is a stronger answer than defining it.
05State versus props, in one line each.
Props are passed in and read-only. State is owned by the component, private to it, and changing it through the setter is what schedules a re-render.
Add the two consequences and it stops sounding memorised: a component cannot change its own props, only ask its parent to render it differently; and one component's state is invisible to every other component unless it is deliberately passed down. Both are the one-way data flow rule showing up in two different places.
Follow-up Can props change?
Yes, constantly - but only because the parent re-rendered with new values. From the child's point of view a props object is immutable; you get a new one next render.
06Why can't a child modify its props?
Because it does not own them. The parent, and probably a sibling or two, hold references to the same value and none of them will be told it changed - so the screen and the data go out of sync with no error.
In development React freezes the props object so the mutation throws rather than silently working, but the real reason is architectural rather than enforced: mutating a prop does not schedule a re-render, so nothing repaints, and when something else eventually re-renders the parent, your change is overwritten by the parent's original value. To change data owned by a parent, take a callback prop and call it.
07What is one-way data flow, and what is the alternative?
Data moves down the tree through props, and events move up through callbacks. The alternative is two-way binding, where a child writes directly into a parent's value.
The trade is debuggability against convenience. Two-way binding is fewer lines for a form; one-way means that for any value on screen there is exactly one component that owns it and one place it could have been set. In a large tree that turns debugging from a search into a walk up the parent chain. Controlled inputs are one-way binding done by hand, and that is why they feel verbose.
08Why use state instead of a plain variable in the component?
Two reasons. A local variable is recreated on every render, so it does not remember anything between them. And assigning to it does not tell React anything, so nothing re-renders.
State solves both: React keeps the value on the fiber, outside the function, and hands it back each render; and calling the setter schedules a render. If you need the first without the second - a value that persists but should not repaint - that is useRef, and it is the correct tool for a timer id or a previous value.
09Why does state look asynchronous?
Because the setter does not assign the variable, it schedules a re-render. The current render's variable was captured when the function ran and cannot change; you get the new value in the next render's call.
It is not asynchronous in the sense of a promise - there is no thenable and no microtask you can await. It is a queued update. Two consequences to have ready: reading state on the line after setting it gives you the old value, and setting the same state twice from the same variable applies the same update twice, so setN(n + 1) twice from 0 gives 1, not 2.
Follow-up How do I get the new value then?
Compute it into a local variable and use that, or use the functional updater form, or in a class pass a second-argument callback. In a function component, useEffect with the value in the dependencies runs after the render that has it.
10When do you need the functional updater form?
Whenever the next state is derived from the previous state - setN(prev => prev + 1). React applies the queued updaters in order against the latest value rather than against whatever the closure captured.
The two cases that force it: several updates in the same tick, and any update from inside a closure that might be stale - an interval, an event listener registered once, a callback wrapped in useCallback with an empty dependency list. The functional form is also what lets you keep that dependency list empty honestly, because you no longer read the state you are updating.
11Can we update state directly? What happens if we do?
You can write this.state.n = 5 or push into a state array and JavaScript will let you. React will not re-render, so the screen goes stale, and the mutation will be silently discarded the next time something else does trigger a render.
With a function component there is a second failure: React compares the new state to the old with Object.is, so handing back the same array or object you mutated looks unchanged and it bails out entirely. That is why the correct move is always a new value - [...items, next], { ...user, name }, items.filter(...) - and why reverse() and sort() and push() on state are bugs, because they mutate in place and return either the same reference or nothing.
Follow-up So immutability is required by React?
Required by the change detection, yes. React compares references, not contents, both for state bail-outs and for React.memo and useMemo dependencies. Mutate and you defeat all three at once.
12Walk me through the class lifecycle.
Mounting: constructor, getDerivedStateFromProps, render, then the DOM is committed, then componentDidMount. Updating: getDerivedStateFromProps, shouldComponentUpdate, render, getSnapshotBeforeUpdate, componentDidUpdate. Unmounting: componentWillUnmount. Plus getDerivedStateFromError and componentDidCatch when a child throws.
Then name which ones you actually use - didMount, didUpdate, willUnmount, and shouldComponentUpdate - and say that the three componentWill* methods are deprecated because interruptible rendering can call them more than once for a single commit. That pair of sentences shows you have used classes rather than read a lifecycle diagram.
13What goes in the constructor?
Calling super(props), setting the initial this.state, and binding methods. Nothing else - no fetching, no subscriptions, no DOM.
The DOM does not exist yet at constructor time, so anything that needs a node has to wait for componentDidMount. With class fields you can skip the constructor entirely: state = { n: 0 } and arrow-function methods do the same job with less ceremony and no binding.
Follow-up Why super(props) and not just super()?
Because this.props is undefined inside the constructor until super has been given them. After the constructor React assigns props itself, so it only matters if you read this.props before super returns - but the rule is cheap to follow.
14Where do you fetch data in a class component, and why not earlier?
componentDidMount. It runs once, after the first commit, when there is a real DOM and the component is definitely staying.
componentWillMount looks earlier and therefore faster, and is wrong for three reasons: it also runs during server rendering where the response never arrives, it cannot be relied on to run exactly once under interruptible rendering, and it does not save you a render anyway - the request is asynchronous, so the first paint has no data either way. In a function component this is useEffect with an empty dependency array.
15What is componentDidUpdate for, and what is the trap?
Reacting to a change - refetching when an id prop changed, or repositioning something after the DOM updated. The trap is that calling setState in it triggers another update, so it must be guarded by comparing prevProps or prevState.
Unguarded, you get an infinite loop that presents as the tab hanging rather than as an error. The guard is literally if (prevProps.id !== this.props.id). The hook version has the same failure shape with a different cause: a useEffect that sets state it also depends on.
16What is shouldComponentUpdate?
A method that returns a boolean deciding whether this update should render at all. Return false and React skips the render and the whole subtree below it for that update.
PureComponent implements it for you with a shallow comparison of props and state, and React.memo is the function-component equivalent. Two warnings worth volunteering: a shallow compare is defeated by a new object or inline function prop, so it very often does nothing; and returning false skips the children too, which means a child whose own state changed still updates - React starts a new pass from that child - but a child receiving new props through you does not.
17getDerivedStateFromProps - what is it and why is it static?
It runs before every render and returns an object merged into state, or null. It is static so it cannot touch this - no instance, no this.props, no side effects.
It exists for one narrow case: state that must reset when a prop changes. Almost every use of it is a mistake, and the documented alternative is better in each: derive the value during render instead of storing it, or reset the state by changing the component's key so React remounts it. Naming the key trick as the preferred fix is the answer that lands.
18What does this.setState actually do, and what is its second argument?
It queues a shallow merge into state and schedules a re-render. The optional second argument is a callback that runs after that re-render has committed, which is the class way to read the new value.
Shallow merge is the difference from useState: setState({ a: 9 }) on state { a: 1, b: 2 } leaves b alone, whereas a useState setter replaces the whole value and you spread it yourself. The callback matters because this.state on the next line is still the old value - the update is queued, not applied.
Follow-up And the functional form?
this.setState((prev, props) => ({ n: prev.n + 1 })). Same rule as hooks: use it whenever the new value depends on the old one, because several queued merges from the same stale this.state all compute the same result.
19How do you give a prop a default value?
In a function component, a default parameter in the destructuring: function Tag({ size = "md" }). In a class, the static defaultProps object.
One trap in both: a default applies only when the value is undefined, never when it is null. Passing label={null} gives you null, not the default, which bites when the value came from an API that uses null for empty. defaultProps on function components is deprecated and removed in React 19 - default parameters are now the only supported form there.
20PropTypes or TypeScript?
TypeScript, for anything new. PropTypes is a runtime check that logs a warning in development only; TypeScript checks at compile time, checks everything else in the file too, and gives you editor completion.
There is one thing PropTypes does that types cannot: validate data that arrives at runtime from outside your program - an API response, a CMS payload. But if that is your worry the right tool is a schema validator at the boundary, not prop types on a component. Note that React 19 removed PropTypes from the React package entirely, so bringing it up as your first answer dates the answer.
21What are the rules of JSX?
One root element per return, className instead of class and htmlFor instead of for, camelCase for attributes and event handlers, every tag closed including self-closing ones, and expressions in braces - expressions, not statements.
Each one has a reason worth having ready: one root because the whole thing compiles to a single function call and a function returns one value; className because class is a reserved word in JavaScript and JSX attributes become object keys; braces take an expression because the contents become an argument, which is why you write a ternary rather than an if.
Follow-up What if I genuinely do not want a wrapper element?
A fragment: <>...</>, or <React.Fragment> when you need a key on it, which the shorthand cannot take. It groups children with no DOM node, which matters inside a table row or a flex or grid container where an extra div changes the layout.
22How do you do conditional rendering?
A ternary for either-or, && for render-or-nothing, an early return for a whole branch, and a variable or a lookup object when the condition has more than two outcomes.
React renders nothing for null, undefined, false and true, so && works and so does returning null. What it does render is 0 and "0", which is the trap in the next question.
23What is wrong with {items.length && <List />}?
When the array is empty, items.length is 0, and 0 && anything evaluates to 0 - which React renders as a literal 0 on the page rather than nothing.
&& returns the left operand when it is falsy, not false. Every other falsy value React skips - null, undefined, false, NaN, empty string - but 0 is a number and numbers are valid content. The fixes are to make the condition a boolean (items.length > 0 &&) or to use a ternary with an explicit null. It shows up most often with counts and with array lengths, which is exactly where it is easiest to miss in review.
24Why does every item in a list need a key?
So React can match each item to the same component instance across renders instead of matching by position. Without one it warns, and reorders or insertions reuse the wrong instance for the wrong data.
It must be stable, unique among its siblings, and derived from the data - an id, or a composed value that identifies the row. Not the index if the list can be reordered, filtered, or inserted into anywhere but the end; and never something generated at render time, which changes every render and remounts every row.
Follow-up Where does the key go if each item is a fragment?
On <React.Fragment key={id}>. The <> shorthand cannot take attributes, so this is the one place you write the long form.
25Composition or inheritance - which does React use, and why?
Composition. React has no meaningful use for component inheritance; the documentation says it has never found a case that needed it. Reuse happens by passing components as children or props, or by extracting a hook.
The concrete version: a Dialog does not subclass a Panel, it renders one and passes content through children. Where an inheritance hierarchy would give you a fixed set of override points, composition gives you a hole the caller fills with anything. And non-visual logic that two components share belongs in a function they both call - a custom hook - not in a base class they both extend.
26What is props.children?
Whatever was written between a component's opening and closing tags, delivered as a normal prop. It is what makes a component a container that does not know what it holds.
It is not always an array - with one child it is that child, with none it is undefined - so use React.Children.map or React.Children.toArray if you need to iterate, since those normalise the shape and skip null and boolean children. Children can also be a function, which is the render-prop pattern, and you can pass elements through any prop name you like; children is just the one with syntax.
27What is a render prop, and is there still a reason to use one?
A prop whose value is a function returning elements, so a component can own behaviour while the caller owns the markup. Hooks replaced most uses of it, but not all.
The case where it still wins is when the shared thing includes an element in the tree, not just logic. A component that owns a DOM node and reports its measured size, a list that owns the scroll container and virtualises it, a form that renders the form element - a hook cannot render on your behalf, so the caller would have to wire up the ref, the container and the layout themselves. If the shared thing is purely stateful logic with no markup, use a hook.
28What is a higher-order component?
A function that takes a component and returns a new one wrapping it - withAuthorization(Settings). It was the pre-hooks way to share behaviour across components.
Three details separate a working HOC from a broken one: spread the props through, or the wrapped component silently loses everything you did not name; set a displayName, or the React DevTools tree is a stack of anonymous wrappers; and forward the ref, because a ref to the wrapper is not a ref to what it wraps. The reasons hooks replaced them are wrapper hell in the tree, prop-name collisions between two HOCs, and no static way to see where a prop came from.
Follow-up Is there anything HOCs still do better?
Wrapping something you do not control, and adding behaviour at the boundary - an error boundary, a permission gate, an analytics wrapper. A hook cannot be added to a component you did not write; a wrapper can.
29What is prop drilling and what is wrong with it?
Passing a value down through components that do not use it, only to hand it further down. Nothing breaks, but every intermediate component gains a prop it does not care about, and renaming the value means editing all of them.
Three fixes, in increasing weight. Compose instead: pass the finished element down as children so the value never has to travel. Use Context for values that are genuinely global to a subtree - theme, current user, locale. Reach for a state library when the state is large, shared and updated from many places. Two levels of drilling is not a problem; five levels through components that are only conduits is.
Follow-up Why not just put everything in Context then?
Because every consumer re-renders when the context value changes, and a single context holding everything means every consumer re-renders on every change. Split contexts by how often they change.
30What is a controlled component?
A form element whose value comes from React state and whose changes go through an onChange handler - React is the source of truth. An uncontrolled one keeps its value in the DOM and you read it with a ref when you need it.
Controlled is the default choice because it makes validation, formatting and disabling a submit button trivial - the value is already in state. Uncontrolled is fewer renders and less code for a form you only read on submit, and is what you want for a file input, which cannot be controlled. The classic bug is a value prop with no onChange, which gives you a field that will not accept typing and a warning about it.
31Two sibling components need the same piece of state. What do you do?
Lift it to their closest common parent, pass the value down to both, and pass a setter down to whichever one changes it.
Say what you would not do: duplicate it in both and keep them in sync with effects, which gives you two sources of truth and a race. If the common parent is many levels up and the intermediate components have no interest in the value, that is the point at which Context or a store earns its place - but lifting comes first, because it needs no new concept.
32Why is a component supposed to be a pure function?
Because React calls it whenever it likes, possibly more than once for one commit, and may discard the result. If the function mutates something outside itself or reads something that changes, those extra calls are visible as bugs.
Concretely, purity means: same props and state produce the same JSX; no mutation of props, of state, or of anything defined outside the component; no fetching, subscribing, timing or DOM writing during render. Anything in that second list belongs in an event handler if it happens because of a user action, or in an effect if it must happen because the component rendered. Strict Mode's double invocation is React telling you when you have broken this rule.
33What is a ref, and when do you use one over state?
A mutable box React keeps for you across renders - useRef in a function, createRef in a class. Writing to ref.current does not re-render, which is exactly the difference from state.
Two jobs: a handle on a DOM node, for focus, scroll, measurement or a media element; and a value that must persist without repainting - an interval id, a previous prop, a mutable flag. The rule for choosing is whether the screen depends on it. If it is displayed, it is state; if it only affects behaviour, it can be a ref.
34What is the difference between an element, a component and an instance?
A component is the function or class you write. An element is the plain object describing one use of it. An instance is what React creates and keeps in memory for a mounted element, holding its state.
This vocabulary is the fastest way to make the rest of the answers precise: <Row /> is an element, Row is a component, the thing holding the row's useState value is an instance. And it explains why identity comes from position and key rather than from the element object - a fresh element is created every render, and React still finds the same instance underneath it.
Predict the output
The output is hidden. Predict it before you open it.
The && trap with a count
const count = 0;
const tags = [];
console.log(count && "shown");
console.log(tags.length && "shown");
console.log(tags.length > 0 && "shown");Show the output
0
0
false&& returns the left operand when it is falsy, and 0 is falsy - so the expression is the number 0, not false. React renders numbers, so {tags.length && <List />} puts a bare 0 on the page for an empty list. React skips false, null and undefined, which is why the third form is the fix.
Two updates from the same value
function Counter() {
const [n, setN] = useState(0);
console.log("render", n);
return (
<div>
<button onClick={() => { setN(n + 1); setN(n + 1); }}>plain</button>
<button onClick={() => { setN((x) => x + 1); setN((x) => x + 1); }}>updater</button>
</div>
);
}
// mount, click plain once, then click updater onceShow the output
render 0
render 1
render 3n is a value captured by this render's closure, so both plain calls compute 0 + 1 and queue the same result - one increment, one render. The updater form queues functions that React applies in order against the running value, so two calls give two increments: 1 becomes 3.
setState merges, and is not immediate
class Box extends React.Component {
state = { a: 1, b: 2 };
componentDidMount() {
this.setState({ a: 9 });
console.log("after setState", this.state.a);
}
render() {
console.log("render", this.state.a, this.state.b);
return null;
}
}Show the output
render 1 2
after setState 1
render 9 2Two things at once. The read on the line after setState still sees 1, because the update is queued and applied before the next render, not now. And b survives without being mentioned: class setState shallow-merges into state, unlike a useState setter, which replaces the whole value.
The setState callback
class Box extends React.Component {
state = { n: 0 };
componentDidMount() {
this.setState({ n: 1 }, () => console.log("callback", this.state.n));
console.log("sync", this.state.n);
}
render() {
console.log("render", this.state.n);
return null;
}
}Show the output
render 0
sync 0
render 1
callback 1The second argument to setState runs after the re-render has been committed, which is the class-only way to do something with the new value. In a function component the equivalent is a useEffect with that value in its dependency array - there is no callback argument on a useState setter.
Mutating state directly
class Box extends React.Component {
state = { n: 0 };
componentDidMount() {
this.state.n = 5;
console.log("mutated", this.state.n);
}
render() {
console.log("render", this.state.n);
return null;
}
}Show the output
render 0
mutated 5The value changed and the screen did not, because nothing told React to render. Worse than a no-op: the component now disagrees with what is on the page, and the next real update will render 5 out of nowhere - or overwrite it, if that update is a setState built from an older copy.
shouldComponentUpdate returning false
class Box extends React.Component {
state = { n: 0 };
shouldComponentUpdate() {
console.log("scu");
return false;
}
componentDidMount() {
this.setState({ n: 1 });
console.log("set");
}
render() {
console.log("render", this.state.n);
return null;
}
}Show the output
render 0
set
scuThe state does become 1 - the update is applied - but render is never called again, so the DOM keeps showing 0. That is the whole hazard of hand-written shouldComponentUpdate: it is a promise that nothing below this point can have changed, and if the promise is wrong the UI is simply stale with no error.
this in an unbound class method
class Btn extends React.Component {
handle() {
console.log(this);
}
render() {
return <button onClick={this.handle}>x</button>;
}
}
// one clickShow the output
undefinedPassing this.handle passes the function, not a bound method - the receiver is lost at the call site, and class bodies are always strict mode so this is undefined rather than the global object. Hence the constructor binding you see in older code, or the class-field arrow form handle = () => {}, which captures this at construction.
Defaults apply to undefined, not null
function Tag({ label = "none" }) {
console.log(JSON.stringify(label));
return null;
}
// <Tag />
// <Tag label={undefined} />
// <Tag label={null} />
// <Tag label="" />Show the output
"none"
"none"
null
""A default parameter fires only for undefined. null is a value and passes straight through, as does the empty string. This is why a field that an API returns as null shows up blank instead of showing your placeholder - coalesce at the boundary with ?? or normalise the response.
Spread order in a wrapper
function Tag(props) {
console.log(props.size, props.color);
return null;
}
const extra = { size: "sm", color: "red" };
// <Tag size="lg" {...extra} />
// <Tag {...extra} size="lg" />Show the output
sm red
lg redJSX attributes become object properties in source order, so the last write wins. In a wrapper component this decides the policy: spread first and your own values override the caller, spread last and the caller overrides you. Most wrappers want the caller to win for className and style, and want to win themselves for the props they exist to control.
children can be a function
function Box(props) {
return props.children(21);
}
const el = <Box>{(n) => n * 2}</Box>;
console.log(typeof el.props.children, el.props.children(21));Show the output
function 42children is an ordinary prop and holds whatever you put between the tags - including a function. That is the entire mechanism behind render props: the component computes something and calls children with it, and the caller decides what to render from it.
A fragment is an element too
const f = <><li>a</li><li>b</li></>;
console.log(f.type === React.Fragment, Array.isArray(f.props.children));Show the output
true trueThe shorthand compiles to an element whose type is the Fragment symbol; React renders its children and creates no DOM node for it. Because the shorthand takes no attributes, a fragment inside a map has to be written as <React.Fragment key={id}> to carry its key.
Write it from memory
Complete, runnable code. Practice typing it from memory before your interview.
A higher-order component that adds an authorization check
The classic HOC question. What is being graded is not the wrapper - it is whether you forward every prop, set a displayName, and forward the ref.
import { createContext, useContext, forwardRef } from "react";
export const AuthContext = createContext({ roles: [] });
export function withAuthorization(Wrapped, requiredRole) {
const Guarded = forwardRef(function Guarded(props, ref) {
const auth = useContext(AuthContext);
const allowed = Boolean(auth) && auth.roles.includes(requiredRole);
if (!allowed) {
return (
<p role="status">
You need the {requiredRole} role to see this.
</p>
);
}
// Every prop through, untouched, plus the ref.
return <Wrapped ref={ref} {...props} />;
});
Guarded.displayName =
"withAuthorization(" + (Wrapped.displayName || Wrapped.name || "Component") + ")";
return Guarded;
}
// ---- using it ----
function Settings({ title, onSave }) {
return (
<section>
<h2>{title}</h2>
<button onClick={onSave}>Save</button>
</section>
);
}
export const AdminSettings = withAuthorization(Settings, "admin");
export function App() {
return (
<AuthContext.Provider value={{ roles: ["admin"] }}>
<AdminSettings title="Billing" onSave={() => {}} />
</AuthContext.Provider>
);
}- {...props} is the whole point. An HOC that names the props it passes through drops every prop it did not think of, and the failure is silent.
- displayName is not cosmetic - without it the DevTools tree is a column of ForwardRef entries and you cannot tell which wrapper is which.
- forwardRef matters because a ref placed on AdminSettings would otherwise point at the wrapper, not at what it wraps. In React 19 ref is an ordinary prop and the forwardRef call is no longer needed; on 18 it is. The wrapped component must itself be able to receive a ref - a host element or another forwardRef.
- If asked what you would do instead today: a useAuthorization hook inside Settings, or an <Authorized role="admin"> component wrapping it at the call site. Reach for an HOC when you are gating something you do not own.
An error boundary
The one thing you still need a class for. Write it once from memory - the two methods, what each is for, and the reset.
import { Component } from "react";
export default class ErrorBoundary extends Component {
state = { error: null };
// Render phase: no side effects, just the state that shows the fallback.
static getDerivedStateFromError(error) {
return { error };
}
// Commit phase: this is where logging belongs.
componentDidCatch(error, info) {
console.error("Boundary caught:", error, info.componentStack);
}
reset = () => this.setState({ error: null });
render() {
const { error } = this.state;
if (error) {
if (typeof this.props.fallback === "function") {
return this.props.fallback(error, this.reset);
}
return (
<div role="alert">
<p>Something went wrong.</p>
<button onClick={this.reset}>Try again</button>
</div>
);
}
return this.props.children;
}
}
// ---- using it ----
export function Page() {
return (
<ErrorBoundary
fallback={(error, reset) => (
<div role="alert">
<p>Could not load the chart: {error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}
>
<Chart />
</ErrorBoundary>
);
}- Two methods, two phases. getDerivedStateFromError is static and runs during render, so it must be pure - return state, do nothing else. componentDidCatch runs after the commit, which is where a logging call is safe.
- It does not catch: errors in event handlers, anything asynchronous (setTimeout, a rejected promise), errors during server rendering, and errors thrown by the boundary itself.
- Place them per region, not one at the root. A boundary around the chart keeps the rest of the page alive; a boundary around the app turns any error into a blank page, which is what you had without one.
- Changing the boundary's key is the other way to reset it - key={route} clears the error on navigation with no reset callback at all.
A render prop, in the case where it still beats a hook
Asked as "when would you use a render prop over a custom hook?". The answer is: when the shared thing includes an element in the tree. Here the component owns the node being measured.
import { useState, useRef, useLayoutEffect } from "react";
export function Measure({ children }) {
const ref = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
const node = ref.current;
if (!node) return;
const observer = new ResizeObserver(([entry]) => {
const box = entry.contentRect;
setSize({ width: box.width, height: box.height });
});
observer.observe(node);
return () => observer.disconnect();
}, []);
return <div ref={ref}>{children(size)}</div>;
}
// ---- using it ----
function Compact() {
return <p>compact layout</p>;
}
function Full() {
return <p>full layout</p>;
}
export function Panel() {
return (
<Measure>
{({ width }) => (width < 400 ? <Compact /> : <Full />)}
</Measure>
);
}- A useMeasure hook could return the size, but it could not render the div - the caller would have to create the element, attach the ref and remember to do both. The component owns the node, so there is nothing to wire up.
- useLayoutEffect rather than useEffect: the first measurement happens before paint, so the caller never renders one frame of the wrong layout.
- The cleanup is the part interviewers look for. A ResizeObserver with no disconnect keeps the node alive after unmount.
- The same shape covers the other surviving cases: a virtualised list that owns the scroll container, or a form that owns the form element and hands field state to its children.