Quiz - JSX and HTML semantics
A teammate keys a list of editable invoice rows by the array index, and it ships:
{rows.map((row, index) => ( <li key={index}> <input defaultValue={row.label} /> </li>))}QA can’t reproduce any bug — the list renders fine, edits save fine. When does this actually break in production?
As soon as a row is deleted, sorted, filtered, or inserted anywhere but the end. React reconciles by key, so a shift in positions makes it reuse the DOM node tied to an index — leaving a half-typed input pinned to a position whose data has changed underneath it.
Immediately on first render — key={index} is a parse-time error because keys must be strings, so the page won’t compile until it’s key={String(index)}.
Only once the list grows past a few hundred rows, where matching by index gets slow enough to drop frames during re-render.
key={row.id}.You need a theme provider that uses React state, so you add 'use client' to the top of app/layout.tsx and wrap {children} in it. The app works. Why does a senior reviewer reject this on sight?
The directive flows downward — it turns the layout and every route beneath it into a client subgraph, forfeiting the Server Components default for the whole app at once. The fix is a small <Providers> child that carries 'use client'; a Server Component can render it and pass children through.
'use client' is illegal in any file that renders <html> or <body>, so the build fails in production even though it runs in dev. Move the directive to app/page.tsx instead.
A 'use client' layout can’t accept the children prop, so the pages render blank. The provider has to read the page from a framework hook instead of from children.
'use client' at the top converts the entire application into client JavaScript, losing the Server Components default globally. It’s the most expensive line you can write in a Next.js app, and it’s invisible in dev because everything still works, just heavier. The correct shape moves the browser-only concern into app/_components/providers.tsx with its own 'use client'; the layout stays a pure Server Component and renders <Providers>{children}</Providers>. The boundary lands on the leaf that needs it. It is not a build error, and a layout accepts children fine.Two snippets render identical pixels — same big bold “Billing” text:
<div className="text-2xl font-bold">Billing</div><h2 className="text-2xl font-bold">Billing</h2>A screen-reader user pulls up the page’s heading list. What’s the difference, and what’s the rule it teaches?
The <div> adds nothing to the heading outline — it has no role in the accessibility tree, so a heading jump sails right past it. Only the <h2> is a navigable node. Heading level is a structural claim set by outline position, never by font size: the class carries the look, the element carries the structure.
There’s no functional difference — Tailwind’s text-2xl font-bold gives both an implicit heading role, so a screen reader announces both as level-2 headings.
The <div> is actually more correct: reserving <h2> for text that’s exactly the second-largest on the page keeps the visual hierarchy and the heading hierarchy in sync.
<div> is a generic box with no role, so it adds zero to the heading outline a screen-reader user navigates like a table of contents — “Billing” is invisible to every heading jump. The <h2> is a real outline node. That’s the rule: a heading level is a structural position in the outline (one <h1>, descending without skips), decided by where the content sits in the document — never by how big the text needs to look. If your <h2> needs to look small or your <h3> large, that’s a separate styling knob. The element carries the outline; the class carries the appearance, and they move independently.You spot this in a code review:
<button onClick={() => router.push('/dashboard')}>Dashboard</button>It works when clicked. What does it cost, and what should it be?
It’s a link wearing button clothes — its whole job is to go to a URL, yet it drops everything a real anchor gives for free: middle-click and Cmd-click to open in a new tab, right-click “Copy link”, crawlability, and being announced as a link. Internal navigation belongs in <Link href="/dashboard">, which adds soft navigation on top of a plain <a href>.
Nothing — a button with a router.push is the idiomatic way to navigate in Next.js, and it’s preferable to <Link> because it gives you a place to run logic before navigating.
The only problem is the missing type — add type="button" so it can’t accidentally submit a surrounding form, and it’s correct.
<button onClick={router.push(...)}> activates on click but isn’t a real anchor, so it silently drops middle-click and Cmd-click new-tab opening, “Copy link address”, and the crawler’s ability to follow it — and it announces as “button”, not “link”. The right tool is <Link href="/dashboard">: it layers soft client-side navigation on top of a plain <a href>, so all the anchor’s free behavior survives. Adding type="button" would fix a different bug (a stray form submit) but wouldn’t make this a real link.The user submits this form without touching any control. Exactly which entries does the browser pack into FormData?
<form> <input type="checkbox" name="newsletter" value="yes" defaultChecked /> <input type="checkbox" name="acceptedTerms" value="true" /> <input type="radio" name="billing" value="monthly" /> <input type="radio" name="billing" value="yearly" defaultChecked /></form>newsletter=yes and billing=yearly — two entries. A checkbox submits its value only when checked, so the unchecked acceptedTerms contributes nothing (its key is simply absent). The radio group shares one name and submits only the checked member’s value, once.
All three names: newsletter=yes, acceptedTerms=false, and billing=yearly — an unchecked checkbox submits its value with false so the server can read the boolean directly.
newsletter=yes, billing=monthly, and billing=yearly — both radios submit their values, and the server takes the last one under the shared name.
value (newsletter=yes); when unchecked it contributes no entry at all — not "false", not an empty value, the key just isn’t in the submission. That asymmetry is why a checkbox can’t be read as a naive boolean on the server; “missing key” is what your Zod schema interprets as “false”. A radio group runs on a shared name: only the checked member submits, once, so you get billing=yearly and the monthly radio contributes nothing. Two entries total.Which of the following are genuine ARIA bugs — markup that misleads or traps a screen-reader user? Select all that apply.
A button whose visible text reads Export carries aria-label="Download CSV".
An <a href="/help"> that a keyboard user can still Tab to carries aria-hidden="true".
An icon-only delete button — no text inside — carries aria-label="Delete invoice".
A decorative chevron glyph rendered beside the word Filters carries aria-hidden="true".
aria-label overrides an element’s accessible name, so putting it on a button that already reads “Export” means the screen reader announces “Download CSV” while the screen shows “Export” — two sources of truth drifting apart; only use it on controls with no visible text. And aria-hidden="true" on a still-focusable link is the classic trap: a keyboard user can Tab onto a control the screen reader has been told to ignore, landing somewhere that announces nothing. The other two are textbook-correct: an icon-only button has nothing to announce until you name it, and hiding a decorative glyph next to a word that already labels it is exactly what aria-hidden is for (and the glyph isn’t focusable, so hiding it is safe).Quiz complete
Score by topic