A20 · Part 3 · Advanced
Reviewing React, and knowing when not to use it
At this level you are judged on what you notice in someone else's code, and on what you argue against.
07On this page
What to look for, in order
A React review that starts at naming and formatting has already missed the expensive problems. Prettier and ESLint handle that layer; a person should be reading for the things a tool cannot see.
| Look for | Because |
|---|---|
| State that should not exist | Derived values stored, or the same truth in two places. This is the most common defect in React by a distance. |
| An effect that is not synchronisation | A user action reacted to after the fact, or a value computed in an effect. Both belong elsewhere. |
| State in the wrong place | At the top of the tree "in case", or duplicated instead of lifted. |
| Missing states | Loading, empty, error. Reviews pass the happy path constantly. |
| Keys | Index keys on anything that can reorder or be deleted from the middle. |
| Accessible names | Icon-only buttons, inputs with a placeholder and no label. |
| Race conditions | A fetch in an effect with no abort or cancellation flag. |
| New dependencies | What does it weigh, who maintains it, and what happens when it stops being maintained. |
Comments that land, and ones that do not
✗ "Use useCallback here."
✓ "This re-creates the handler each render, and Row is memoised — so the memo
never hits. Worth a useCallback, or moving the state into SearchBox."
✗ "This is wrong."
✓ "If userId changes twice quickly, the slower response can land last and
overwrite the newer one. An AbortController in the cleanup fixes it."
Name the consequence, not the rule. A reviewer who cites a rule gets compliance; one who describes the failure gets a colleague who will catch it themselves next time. And say which comments are blocking — leaving that ambiguous is how a review turns into three days of back and forth.
The review question worth asking every time
"What happens the second time?" The second render, the second click, the second user, the second tab. Most React bugs that reach production are correct the first time through and wrong after that — stale closures, missing cleanups, races, duplicated state that has not drifted yet.
What not to block on
Personal preference about file structure, arrow versus function declaration, whether a component takes three props or four, and premature memoisation. A review that blocks on those trains people to stop asking for reviews.
The security review, specifically
React escapes everything you interpolate, which removes the most common XSS vector by default. The vulnerabilities that remain are the ones where you left that protection, and a reviewer should know all four.
| Risk | What to look for |
|---|---|
dangerouslySetInnerHTML | Any use at all. If the HTML is not authored by you, it must be sanitised with DOMPurify first — and sanitised on the way in, not on the way out. |
| URLs from data | <a href={user.website}> with javascript: in it executes on click. Validate the protocol. |
| Spreading unknown props | <div {...fromApi} /> can set event handlers and dangerouslySetInnerHTML. Pick fields explicitly. |
| Secrets in the bundle | Anything prefixed VITE_ or NEXT_PUBLIC_ is public. A key in client code is a published key. |
function SafeLink({ href, children }) {
const ok = /^(https?:|mailto:|/)/i.test(href ?? "");
return ok ? <a href={href}>{children}</a> : <span>{children}</span>;
}
Two more worth a glance in review: tokens in localStorage are
readable by any script that gets injected, so an httpOnly cookie is the safer
default; and a Content Security Policy is a seatbelt that limits the damage
when one of the four above slips through — not a substitute for any of
them.
Trusted Types now work with React
Content-Security-Policy: require-trusted-types-for 'script'
With that header, the browser refuses raw strings at injection sinks such as
innerHTML and only accepts TrustedHTML,
TrustedScript or TrustedScriptURL objects created by a
policy you control. Before React 19.3, React turned every value into a string
with '' + value before touching the DOM, so a trusted object became
a plain string the browser rejected — and teams gave up on the policy.
Since 19.3 React passes these objects through unchanged.
const policy = trustedTypes.createPolicy("sanitise", {
createHTML: (dirty) => DOMPurify.sanitize(dirty),
});
<div dangerouslySetInnerHTML={{ __html: policy.createHTML(comment) }} />
The review question becomes: is every dangerouslySetInnerHTML fed by
a policy? Once the header is on, a raw string there fails loudly in the browser
instead of shipping.
Dependencies are part of the review
React2Shell (CVE-2025-55182, CVSS 10.0) let an unauthenticated request run code on any server using React Server Components, and it was exploited within two days of disclosure. For any app with a server boundary, a reviewer should check the React and framework versions against current advisories — 19.0.4, 19.1.5 or 19.2.4 and later for that family of flaws — not only the diff in front of them.
When not to use React
Being able to argue against your own default is most of what "senior" means in this conversation. React is a poor fit more often than the industry admits.
| Situation | Better |
|---|---|
| A content site with a few interactive pieces | Astro, or plain HTML with islands. Shipping a framework to render an article is a strange trade. |
| A form and a table on an internal tool | Server-rendered HTML with htmx or Hotwire. No build step, no state duplication. |
| A highly interactive canvas or editor | The DOM is not the bottleneck; React's model may not help. Consider owning the render loop. |
| A tiny widget embedded on other people's pages | Preact, or no framework. Bundle size dominates. |
| A team that knows something else well | The framework they know. Familiarity beats theoretical fit almost every time. |
Where React genuinely earns it: a large application with a lot of shared, stateful UI; a team that needs a common component vocabulary; a product where the same components must run on web and native; and a hiring market where React knowledge is easy to find.
Arguing the trade honestly
"React is fine here" ✗ // a preference
"This is twelve pages of mostly static content with three interactive
widgets. React costs us ~45 KB of runtime plus hydration on every page
to make three components work. Islands would ship the same UI with
almost no JavaScript. If we expect this to grow into an application,
React is the right bet now — otherwise it is not." ✓
The second version names the cost, the benefit, and the condition under which the answer flips. That structure — cost, benefit, what would change my mind — is what a design review is scoring, and it applies to every tool choice, not just this one.
The things worth having an opinion on
- Server-first by default, with interactivity as islands — and knowing what that costs in complexity.
- Server state is not client state, and a query cache is not optional at any real size.
- Effects are a smell until proven otherwise.
- Accessibility is a correctness requirement, not a phase.
- Measure before optimising, and delete the optimisation that did not help.
You do not have to agree with all five. You should be able to say where you stand on each and why, because that is the conversation a senior React interview actually is.
Interview answer, one sentence
"In review I read for state that should not exist, effects that are not synchronisation, and what happens the second time — the first render is usually fine. And I would argue against React for a mostly static site: it costs a runtime and hydration on every page to make three widgets work, and islands ship the same UI with almost no JavaScript."
Opens in the editor: write it, run it, and check it against real tests.
All 54 for this level →Finished reading?
0 of 57 chapters read in this series