Verification tools
This course has no in-browser playground for React the way a runnable sandbox would give you — the only way to prove a claim about generated code is a real project, a real tsc/eslint run, or a real test. Every output below is real, captured against React 19.3.0 and [email protected] in this course’s own Vite + TypeScript + Vitest + React Testing Library probe. None of these tools overlap perfectly, and none is close to complete on its own — the AI Bug Catalog’s “Caught by” line for each of its 25 entries names exactly which one (if any) of the tools below would have caught it.
tsc --noEmit — the fast, first pass, strict
Section titled “tsc --noEmit — the fast, first pass, strict”Type-checking alone catches surprisingly little of this catalog by design — most of React’s own rules (the Rules of Hooks, effect cleanup, key identity, memoization) are runtime/behavioral concerns TypeScript’s type system has no way to see. What tsc does catch reliably, under a strict config: a useRef<T>() call with no argument (React 19’s typings dropped the zero-argument overload), and a component return type annotated JSX.Element/ReactElement in a branch that also returns null.
npx tsc --noEmitBadUseRefNoArg.tsx(3,15): error TS2554: Expected 1 arguments, but got 0.StatusIcon.tsx(3,44): error TS2322: Type 'null' is not assignable to type 'Element'.Both are real, current errors under @types/react@19 — not style nitpicks. See the AI Bug Catalog, entries 4 and 25.
[email protected] — the compiler’s own diagnostics as lint errors
Section titled “[email protected] — the compiler’s own diagnostics as lint errors”Since the Vite react-ts template ships oxlint, not ESLint, the Hooks-aware rules need an explicit install and a flat config:
npm install -D eslint @typescript-eslint/parser eslint-plugin-react-hooksimport reactHooks from 'eslint-plugin-react-hooks';import tsParser from '@typescript-eslint/parser';import { defineConfig } from 'eslint/config';
export default defineConfig([ { files: ['**/*.{ts,tsx}'], languageOptions: { parser: tsParser } }, reactHooks.configs.flat['recommended-latest'],]);recommended-latest turns on the classic rules-of-hooks/exhaustive-deps rules and the React Compiler’s own diagnostics (refs, set-state-in-render, immutability, and more) in one preset. Five real violations, run through that exact config, one per rule this module’s catalog needs:
rules-of-hooks — a hook called conditionally (AI Bug Catalog, entry 3):
Toggle.tsx 5:25 error React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooksexhaustive-deps — a missing dependency:
Counter.tsx 8:6 warning React Hook useEffect has a missing dependency: 'step'. Either include it or remove the dependency array. If 'setCount' needs the current value of 'step', you can also switch to useReducer instead of useState and read 'step' in the reducer react-hooks/exhaustive-depsrefs — reading/writing ref.current during render (useRef & the DOM):
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only beaccessed outside of render, such as in event handlers or effects. Accessinga ref value (the `current` property) during render can cause your componentnot to update as expected (https://react.dev/reference/react/useRef).set-state-in-render — setState called unconditionally during render (AI Bug Catalog, entry 17):
BadSetState.tsx 6:3 error Error: Cannot call setState during render
Calling setState during render may trigger an infinite loop.* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders* To derive data from other state/props, compute the derived data during render without using state.
4 | export function BadSetState() { 5 | const [count, setCount] = useState(0)> 6 | setCount(count + 1) | ^^^^^^^^ Found setState() in render 7 | return <p>{count}</p> 8 | } 9 | react-hooks/set-state-in-renderimmutability — mutating a prop directly (AI Bug Catalog, entry 18):
Themed.tsx 4:3 error Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using alocal variable instead.
2 | 3 | export function Themed({ config }: { config: Config }) {> 4 | config.theme = 'dark' | ^^^^^^ value cannot be modified 5 | return <p>{config.theme}</p> 6 | } 7 | react-hooks/immutabilityAll five are real, from the identical config above — no hand-written error text.
babel-plugin-react-compiler — reading a real bail-out
Section titled “babel-plugin-react-compiler — reading a real bail-out”The compiler is deliberately conservative: it either fully memoizes a function (a _c() memo-cache call wraps every cacheable sub-expression) or leaves it completely untouched, with no build error either way. Compiling the exact BadSetState function from the set-state-in-render example above with [email protected] produces the function passed through byte-for-byte:
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";function BadSetState() { const [count, setCount] = useState(0); setCount(count + 1); // setState during render — Rules of React violation return /*#__PURE__*/_jsxDEV("p", { children: count }, void 0, false);}No _c(), no memo cache — the bail-out is silent by design. Compiling the Themed mutation example produces the identical shape: passed through untouched. The only way to know a component was skipped is to diff the compiled output against a known-good one, or to run the ESLint rules above, which report the same violations as real errors, at edit time, without needing to read compiler output at all. See The React Compiler for the full input/output pair, plus what a component the compiler does memoize looks like.
RTL + Vitest: a real smoke test
Section titled “RTL + Vitest: a real smoke test”A rendered, interacted-with component is still the most direct proof this course has for “does this actually work”:
import { useState } from 'react';
export default function SmokeCounter() { const [count, setCount] = useState(0); return <button onClick={() => setCount((c) => c + 1)}>count: {count}</button>;}import { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';import { expect, test } from 'vitest';import SmokeCounter from '../components/SmokeCounter';
test('a real RTL smoke test: click, then assert on what the user sees', async () => { const user = userEvent.setup(); render(<SmokeCounter />); await user.click(screen.getByRole('button', { name: /count: 0/i })); expect(screen.getByRole('button', { name: /count: 1/i })).toBeInTheDocument();});npm run verify -- --test reading-react/verification-tools✓ a real RTL smoke test: click, then assert on what the user sees
Test Files 1 passed (1) Tests 1 passed (1)See Testing for the full RTL/Vitest discipline this course follows — queries, userEvent, the “not wrapped in act” warning, and testing Suspense/async transitions.
<Profiler onRender> — real commit counts, not a guess
Section titled “<Profiler onRender> — real commit counts, not a guess”<Profiler id="..." onRender={...}> calls its callback once per commit inside it, with a phase ("mount" or "update") and real timing data — the mechanical way to confirm “did this actually re-render,” instead of eyeballing it:
import { Profiler, useState } from 'react';import { render, cleanup, act } from '@testing-library/react';import { afterEach, expect, test } from 'vitest';
afterEach(cleanup);
function ProfiledChild({ label }: { label: string }) { return <span>{label}</span>;}
function ProfiledParent() { const [tick, setTick] = useState(0); return ( <div> <button onClick={() => setTick((t) => t + 1)}>{tick}</button> <ProfiledChild label="static" /> </div> );}
test('<Profiler onRender> records one commit per actual re-render, with real phase/duration data', () => { const commits: { phase: string }[] = []; const { getByRole } = render( <Profiler id="parent" onRender={(_id, phase) => commits.push({ phase })}> <ProfiledParent /> </Profiler>, );
expect(commits).toHaveLength(1); expect(commits[0].phase).toBe('mount');
act(() => { getByRole('button').click(); }); act(() => { getByRole('button').click(); });
expect(commits).toHaveLength(3); expect(commits.slice(1).every((c) => c.phase === 'update')).toBe(true);});Mounting a component wrapped in <Profiler> produces exactly one commit (phase: 'mount'); two subsequent state-driven re-renders produce exactly two more (phase: 'update') — three commits total, proven by asserting the callback’s own call count, not inferred from behavior. This is the same primitive React DevTools’ own Profiler tab is built on. See Rendering Performance for <Profiler> used against a real optimization.
React DevTools: Components and Profiler tabs
Section titled “React DevTools: Components and Profiler tabs”The browser extension gives you two tabs this course’s command-line tools cannot replace:
- Components — inspect the live component tree, its props, state, and every hook’s current value, in the order the hooks were called; select a component to see exactly what it received and is holding, without adding a single
console.log. - Profiler — record a session of interactions and see a flame graph of every commit: which components rendered, how long each took, and (critically) why each one rendered — DevTools names the specific prop or state change that triggered it, which is the fastest way to confirm entry 19 or entry 24’s memoization bugs on a real page instead of a unit test.
- “Highlight updates when components render” (a Profiler-tab setting) — flashes a border around every component that re-renders, live, as you interact with the page. Turning this on over a page suspected of over-rendering (a wide, unmemoized Context; an inline object breaking a
memoboundary) makes the scope of the problem visible in seconds, before you write a single test.
<StrictMode> in development
Section titled “<StrictMode> in development”<StrictMode> deliberately double-renders components and double-invokes effects’ setup→cleanup→setup in development — proven, not just described, in Rendering & Reconciliation:
✓ StrictMode (dev) renders twice and runs setup -> cleanup -> setup once on mount✓ without StrictMode, the same component renders and effects run exactly onceLeaving it on costs nothing in production (it does nothing there) and surfaces exactly the kind of impurity this module’s catalog is full of — a mutation during render, an effect whose cleanup doesn’t fully undo its setup — as a development-time symptom, before either one ships. See Production & Ecosystem for the dev/production build split this relies on.
react-dom/static’s prerender — inspecting the exact HTML a user’s first paint will show
Section titled “react-dom/static’s prerender — inspecting the exact HTML a user’s first paint will show”react-dom/static’s prerender (and prerenderToNodeStream) fully waits out all data before returning — unlike renderToString/renderToPipeableStream, it never emits a partial shell, so what you get back is the exact, complete HTML a real first paint will show:
function StaticPage() { return ( <html> <body> <p data-testid="marker">static</p> </body> </html> );}
export default StaticPage;import { prerender } from 'react-dom/static';import { expect, test } from 'vitest';import StaticPage from '../components/StaticPage';
test('react-dom/static prerender() resolves a real, complete HTML stream (Node runtime)', async () => { const { prelude } = await prerender(<StaticPage />); const reader = prelude.getReader(); const decoder = new TextDecoder(); let html = ''; for (;;) { const { done, value } = await reader.read(); if (done) break; html += decoder.decode(value); } expect(html).toContain('<p data-testid="marker">static</p>');});Its value for hydration bugs specifically: inspecting this exact, final, awaited HTML at build time — before any client JavaScript runs — is what lets you catch a value that will disagree the moment the client re-executes the same component, entry 22’s Date.now()/Math.random() pattern being the clearest case. prerender cannot run the client-side re-execution itself (that needs a real browser or hydrateRoot, proven separately in the AI Bug Catalog’s entry 22) — but reading its output is the cheapest place to notice a value that has no business being stable in the first place.
This course’s own harness: compile every snippet
Section titled “This course’s own harness: compile every snippet”Every code fence in this course tagged with a bare // path.ext first line is a real file, collected by tools/verify-snippets.mjs into a namespaced directory per lesson and type-checked in one tsc --noEmit pass across the whole course:
node tools/verify-snippets.mjsno type errors.
Per-module fence summary (collected / skipped-no-path / expect-error / tests): reading-react: ... (see this module's own gate run) ...A fence deliberately showing broken or misbehaving code (like most of the “code AI typically writes” examples in the AI Bug Catalog) starts with // @expect-error <reason> so the harness skips collecting it — the lesson prose carries the real, separately-captured tsc/eslint/console output instead of asking the harness to reproduce a deliberate failure on every run.
Mental Model
Section titled “Mental Model”flowchart TD
subgraph Static["Static (source only)"]
Tsc["tsc --noEmit<br/>real type errors: useRef arg, JSX.Element/null"]
Eslint["eslint-plugin-react-hooks@7<br/>rules-of-hooks, exhaustive-deps, refs,<br/>set-state-in-render, immutability"]
Compiler["babel-plugin-react-compiler<br/>read the real bail-out (or lack of one)"]
Verify["tools/verify-snippets.mjs<br/>compiles every tagged fence in the course"]
end
subgraph Runtime["Runtime (needs a real render or commit)"]
Rtl["RTL + Vitest<br/>click, then assert on what a user sees"]
Profiler["<Profiler onRender><br/>real per-commit counts, phase, timing"]
Devtools["React DevTools<br/>Components + Profiler tabs, highlight updates"]
Strict["<StrictMode><br/>dev-only double render/effect"]
end
subgraph Framework["Needs a real SSR/hydration setup or framework"]
Static["react-dom/static prerender<br/>exact final HTML before any client JS runs"]
Rsc["a real RSC framework build<br/>'use client'/'use server' boundary enforcement"]
end