ssr-hydration-streaming-diagnosis
Diagnoses hydration-mismatch errors and streaming/Suspense-boundary structural issues to their specific root cause — non-deterministic rendering source, missing error boundary, serial data-fetch waterfall, or premature auth-unchecked streaming — grounded in the exact React/Next.j
Install
npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/ssr-hydration-streaming-diagnosis
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
git clone https://github.com/VincentChuWaiChow/vanguard-frontier-agentic.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vincentchuwaichow/vanguard-frontier-agentic collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
SSR Hydration & Streaming Diagnosis
Purpose
Diagnose hydration-mismatch errors and streaming/Suspense-boundary structural issues to their specific root cause, without collapsing every hydration warning into a reflex suppressHydrationWarning fix or every streaming complaint into "wrap it in Suspense." This skill exists so root-cause identification — non-deterministic rendering source, missing error boundary, serial fetch waterfall, or auth-unchecked premature streaming — happens before any fix is proposed, and so that fix proposal never substitutes for diagnosis.
When to use
Use this skill when the user asks to:
- diagnose a hydration-mismatch error or console warning (React 18 warning-style or React 19 diff-style),
- investigate a TTFB/LCP regression that followed an SSR or streaming change,
- review why a Suspense-wrapped section crashes to blank instead of degrading gracefully,
- review a new Suspense/error-boundary tree design before it ships.
Do not use this skill for:
- state/store serialization design decisions with no specific mismatch or streaming symptom — that is
state-management-decision-review, - route-tree or code-splitting review unrelated to SSR/streaming mechanics — that is
routing-navigation-review, - generic component decomposition or state-placement review — that is
react-component-architecture-review.
Context7 Documentation Protocol
- Resolve
/reactjs/react.devbefore diagnosing any hydration-mismatch error. The diagnostic error format itself is version-specific: React 19 emits a single detailed diff-style error (Hydration failed because the server rendered HTML didn't match the client...with a+ Client/- Serverdiff), while React 18 emits separate, less-specific warnings and silently "patches" mismatched nodes instead of remounting from the nearest Suspense boundary. Diagnosing a React 18 warning against the React 19 mental model (or vice versa) produces a wrong root-cause guess. - Before recommending a fix, confirm the installed React major version (check
package.json/ lockfile) and callquery-docsscoped to that version for "hydration mismatch" and, ifuse()or a Suspense-wrapped fetch is involved, for "use hook Suspense error boundary." - Resolve
/vercel/next.jsand callquery-docsscoped to the confirmed Next.js version before asserting streaming/Suspense-boundary semantics,loading.jsbehavior, or bot/crawler streaming exceptions — Next.js waits for all data fetching to finish before sending a fully rendered page to bots/crawlers instead of streaming progressively, which changes what "premature streaming" even means for that request path. - If Context7 is unavailable, fall back to the
official_docsURLs in this skill'smetadata.jsonand label every claimdocumentation-based, unverified against current release.
Lean operating rules
- Root cause before fix, always. Do not propose
suppressHydrationWarning, a Suspense-boundary restructure, or a caching change until the specific mechanism is named and evidenced. A fix proposed before root cause is identified is a guess wearing a diagnosis costume. - The four canonical hydration-mismatch causes per React's own diagnostic message are: a server/client branch (
typeof window !== 'undefined'), variable input (Date.now(),Math.random()), locale-dependent formatting that differs between server and client, and external/changing data rendered without a matching snapshot sent alongside the HTML — plus invalid HTML tag nesting as a fifth, structural cause. Map the reported mismatch to one of these five before naming a fix. suppressHydrationWarningis a one-level-deep escape hatch, not a fix. It is acceptable only when the root cause is a genuinely unavoidable non-determinism (for example, a legitimately locale/timezone-dependent timestamp) and only with a documented justification comment at the call site. Treat any use of it without an identified root cause and written justification as a rejected finding, not an approved one.- Since React 18, a hydration mismatch from missing/extra text content is treated as an error, not a soft warning: React discards and re-renders client-side from the nearest
<Suspense>boundary rather than patching individual nodes. This means the blast radius of a mismatch is bounded by Suspense placement — a missing or too-coarse Suspense boundary turns a small mismatch into a large client re-render. - Every component that suspends via
use()on a Promise (or any Suspense-triggering data read) must have a paired Error Boundary somewhere in its ancestor tree, because a rejected Promise propagates to the nearest Error Boundary, not the nearest Suspense fallback. A Suspense boundary with no ancestor Error Boundary is a crash-to-blank defect, not a style preference. - Suspense boundaries that are too coarse (one boundary wrapping an entire page, mixing fast and slow content) block fast content behind the page's slowest fetch. Prefer sibling Suspense boundaries around independently-loading sections so each streams in as its own data resolves, per Next.js's own parallel-streaming pattern.
- A fetch that depends on the result of a prior fetch (for example, fetching playlists that require an artist ID from a preceding fetch) is legitimately sequential — do not flag that as a waterfall defect. Only flag fetches that are data-independent but are still awaited one after another instead of started concurrently.
- Streaming that flushes page structure or partial content to the client before an authorization check has resolved is a potential information-disclosure defect, not a performance nitpick — escalate it, do not file it as a UX note.
- Never execute, build, or run application code as part of this review; this is a static-review skill (Read/Grep/Glob only) — diagnose from the reported error text, the component/boundary source, and any network/fetch-order trace the user provides.
References
Load these only when needed:
- Hydration mismatch diagnosis — use when diagnosing a specific hydration-mismatch error or warning to its root cause.
- Suspense and streaming structure review — use when reviewing or designing a Suspense/error-boundary tree, or diagnosing a TTFB/LCP regression or crash-to-blank symptom.
- Fetch waterfall and auth-timing review — use when diagnosing a sequential-fetch performance issue or reviewing whether streaming exposes content before authorization resolves.
Response minimum
Return, at minimum:
- the specific root-cause mechanism identified (or explicit statement that root cause is unresolved and more evidence is needed — never a guess presented as a diagnosis),
- the React/Next.js major version the diagnosis was verified against,
- evidence level (
documentation-based,repo evidence,user-provided evidence, orinference) and what Context7 query grounded the claim, - the proposed fix scoped to the identified root cause, or explicit refusal to propose a fix until root cause is confirmed,
- security/disclosure caveat if streaming-before-auth risk is present,
- verdict: approve / approve-with-notes / block.
Files (vanguard-frontier-agentic)
-
references
-
fetch-waterfall-and-auth-timing.md 6.2 KB
# Fetch Waterfall and Auth-Timing Review Use this reference when diagnosing a sequential-fetch performance issue, or when reviewing whether streaming exposes page structure or content before an authorization check has resolved. ## What people get wrong Two separate bad assumptions show up together often enough to warrant one reference: > "These fetches run one after another because that's just how `await` works" — sometimes true, sometimes a waterfall defect. > "Streaming just sends HTML progressively, auth isn't a streaming concern" — wrong when the streamed content itself is the thing that needed authorization. Both require actually tracing the await order and the request lifecycle, not pattern-matching on the presence of `await` or `<Suspense>`. ## Waterfall diagnosis: sequential vs. genuinely dependent Not every sequential `await` chain is a defect. Next.js's own data-fetching guidance shows a legitimately sequential case: fetching an artist's playlists requires the artist's ID, which only exists after the artist fetch resolves. That is a genuine data dependency — the second fetch cannot start before the first resolves, and flagging it as a waterfall defect is a false positive. The defect pattern is different: two (or more) fetches that do **not** depend on each other's output, but are still written with sequential `await` calls, so the second fetch doesn't start until the first finishes — costing the sum of both latencies instead of the max. ### Diagnostic procedure 1. For each pair of fetches in the flagged code path, determine: does fetch B use any value produced by fetch A (an ID, a token, a computed parameter)? If yes, sequential `await` is correct — do not flag it. 2. If fetch B does not depend on fetch A's output but is still written after `await fetchA()`, this is a real waterfall. Recommend starting both concurrently — begin the fetch (don't await it immediately) and consume both results together, or use per-section Suspense boundaries with each section starting its own fetch independently so they resolve in parallel rather than being serialized by a shared parent awaiting both in sequence. 3. Confirm the recommended restructuring doesn't accidentally introduce a request that now fires on every render when it previously ran once — verify fetch caching/memoization semantics for the confirmed framework version via Context7 before asserting the parallelized version is still correct. ### Fix framing Do not just say "parallelize this." State which specific fetches are independent (by name/file:line), what the current serialized cost is (sum of both fetches' latency), and what the corrected cost would be (max of both). A vague "these could probably be parallel" is not a completed diagnosis. ## Auth-timing and premature streaming Streaming sends the response progressively as chunks resolve, which means part of the response can reach the client (and be rendered, or at minimum be visible in dev tools / network trace) before the entire request lifecycle — including any authorization check — has completed, if the authorization check is not structured to gate the stream's start. ### The specific defect pattern A route or component streams protected content (page structure that reveals a resource exists, partial data, or a shell that implies the current user has access) **before** the code path that verifies the requester is authorized to see that content has resolved and short-circuited on failure. This differs from a plain performance concern: the risk is that an unauthorized request can observe something about a protected resource — its existence, its shape, or fragments of its data — purely from what streamed before the authorization check would have blocked it. ### Review procedure 1. Identify the authorization check for the route/resource in scope (session/role/ownership verification). 2. Trace what streams (renders, sends data, or resolves a Suspense boundary) before that authorization check has run and been evaluated. 3. If anything protected-resource-specific streams before the authorization check resolves and can fail, treat this as a potential information-disclosure finding and escalate — do not file it as a performance observation. 4. Distinguish this from the unprotected-shell case: a generic loading skeleton with no resource-specific information (no IDs, no titles, no counts) streaming before auth resolves is not a disclosure risk by itself. The defect is specifically protected-resource-specific content or metadata reaching the client stream ahead of the authorization gate. 5. Note the bot/crawler exception when relevant: Next.js does not stream progressively to bots/crawlers — it waits for full data resolution first — so a premature-streaming disclosure concern for a browser request does not automatically apply the same way to a bot-facing response, and conflating the two produces an inaccurate threat model. ### Fix framing The correct fix is almost always structural: perform the authorization check synchronously before starting the response (or before the first byte that carries resource-specific content is flushed), not "add a loading spinner" or "move the check earlier in the component tree" without confirming it actually runs before the first protected content leaves the server. ## Adversarial checklist - For each flagged sequential-fetch pair: is there an actual data dependency, or is this a real waterfall? - For the proposed parallelization: does it change caching/memoization semantics for the confirmed framework version? - Does any protected-resource-specific content (not a generic skeleton) stream before the authorization check has resolved and could fail? - Is the auth-timing concern being applied correctly to the browser-facing path, given that bot/crawler responses do not stream progressively in the first place? ## When to push back Push back if the user asks to: - "just parallelize everything" without tracing which fetches are actually independent, - move an authorization check later in the render tree "to make the page feel faster," when that change would let protected content start streaming before the check resolves, - treat a generic loading skeleton with zero resource-specific data as an auth-timing risk — that is a false positive that dilutes real findings. -
hydration-mismatch-diagnosis.md 6 KB
# Hydration Mismatch Diagnosis Use this reference when a hydration-mismatch error or warning has been reported and the root cause is not yet identified. ## What people get wrong The reflex move is: > There's a hydration warning in the console. Add `suppressHydrationWarning` and move on. That is not a diagnosis. It is silence applied to a symptom. `suppressHydrationWarning` only works one level deep, does not make the underlying value consistent, and React explicitly documents it as an escape hatch that should not be overused. Applying it before naming the mechanism means the next mismatch — possibly a real one, possibly deeper in the tree — gets silently absorbed into the same reflex. ## Version-specific error format — check this first The shape of the error itself tells you which React major version you are diagnosing against, and the two shapes carry different diagnostic information: - **React 19+**: a single detailed error with an inline diff (`+ Client` / `- Server`) pinpointing the exact mismatched node, plus an enumerated list of the five canonical causes in the error text itself, plus a link to `https://react.dev/link/hydration-mismatch`. - **React 18**: hydration mismatches from missing/extra text content are treated as errors (not silently patched), and React reverts to client rendering up to the closest `<Suspense>` boundary — but the console output is less specific about *which* node and *why* than React 19's diff-style message. - **Pre-18**: React would attempt to "patch up" individual mismatched nodes on the client — a materially different (and less safe) recovery behavior than 18+. Confirm the installed React major version before interpreting the error text. Diagnosing a React 18 mismatch as if it carries a React 19 diff, or vice versa, leads to misreading what the error is actually telling you. ## The five canonical causes React's own diagnostic message enumerates these. Map the specific reported mismatch to one of them before proposing anything: 1. **Environment branch** — `typeof window !== 'undefined'` (or equivalent) used to render different output on server vs. client. 2. **Variable input** — `Date.now()`, `Math.random()`, or any value that changes between the server render and the client's initial render. 3. **Locale-dependent formatting** — `toLocaleDateString()`, `Intl.NumberFormat`, or similar, where the server's locale/timezone differs from the client's. 4. **Unsent external state** — data that changed between server render and client hydration, rendered without the server sending a snapshot of the value it used alongside the HTML. 5. **Invalid HTML nesting** — structurally invalid tag nesting (e.g., a `<div>` inside a `<p>`) that the browser silently repairs during parsing, producing a DOM tree that no longer matches what React expects to hydrate. A sixth, non-code cause exists and should not be misdiagnosed as a code defect: a browser extension mutating the DOM before React hydrates. If the mismatch is isolated to attributes an ad-blocker or password-manager extension is known to inject, say so explicitly rather than chasing a phantom code path. ## Diagnostic procedure 1. Confirm the React major version. 2. Read the full error/diff text (React 19) or reproduce and inspect the DOM/console output (React 18) — do not diagnose from a truncated paste. 3. Search the flagged component (and its ancestors up to the nearest previous hydration boundary) for each of the five causes in order: environment branches, non-deterministic values, locale formatting calls, externally-sourced data without a matching server-sent snapshot, and invalid nesting. 4. State the identified cause explicitly, citing the file:line and the specific expression responsible. 5. Only after the cause is named, propose a fix scoped to that cause — see fix mapping below. ## Fix mapping — do not default to suppression | Root cause | Correct fix | `suppressHydrationWarning` acceptable? | |---|---|---| | Environment branch (`typeof window`) | Defer the branch to `useEffect`/client-only render after mount, or use a library-provided SSR-safe check | No | | `Date.now()` / `Math.random()` | Compute the value once on the server and pass it down as a prop/serialized snapshot; do not recompute on the client | No | | Locale/timezone formatting | Pass the server's resolved locale/timezone explicitly instead of relying on ambient `Intl` defaults, or accept the value as genuinely unavoidable | Only if genuinely unavoidable, with a written justification comment | | Unsent external/changing data | Send a snapshot of the data used for the server render down to the client so the client's first render matches it | No | | Invalid HTML nesting | Fix the markup structure | No | | Browser extension interference | No code fix; document as an environment artifact, do not chase it as a defect | N/A | The only row where `suppressHydrationWarning` is legitimate is the locale/timezone row, and only when the mismatch truly cannot be eliminated (for example, a "time ago" display that is inherently observer-relative) — and even then, it requires a comment at the call site stating why suppression is correct, not merely convenient. ## Adversarial checklist Before closing a hydration-mismatch diagnosis, answer these: - Which of the five canonical causes (or the extension exception) does the evidence actually point to — not which one is the easiest to write a fix for? - Was the diagnosis made against the correct React-major-version error format? - If the fix is `suppressHydrationWarning`, is there a written justification, and is the non-determinism actually unavoidable rather than just inconvenient to eliminate? - Does the fix address the value/branch that differs, or does it just make the warning go away while the divergent values still exist? - Is this an isolated single-node mismatch, or does the same root cause recur across multiple components (suggesting a shared utility/hook is the real source)? If these cannot be answered, the diagnosis is not complete — say so rather than proposing a fix. -
suspense-streaming-structure.md 6.4 KB
# Suspense and Streaming Structure Review Use this reference when reviewing or designing a Suspense/error-boundary tree, or diagnosing a TTFB/LCP regression or a crash-to-blank symptom tied to streaming. ## What people get wrong The naive story is: > Wrap it in `<Suspense>` and streaming just works. Incomplete. A `<Suspense>` boundary controls *what shows while content is pending* and *how far a hydration-mismatch re-render blast radius reaches*. It does not, by itself, handle rejection (that is the Error Boundary's job), does not make sibling sections stream independently unless they are wrapped in their own boundaries, and does not change fetch ordering — a slow fetch inside a narrow Suspense boundary is still slow; the boundary just contains the wait to that section instead of blocking the whole page. ## Officially grounded shape - `use()` reading a pending Promise causes the calling component to suspend; React shows the nearest `<Suspense fallback>` while pending. - If the Promise **rejects**, the error propagates to the nearest **Error Boundary**, not the nearest Suspense fallback. A component that calls `use()` (or otherwise suspends on a rejectable data source) with a Suspense boundary but no ancestor Error Boundary will crash to an unhandled error when that data source fails — there is no fallback UI for the rejection case. - Since React 18, a hydration mismatch causes React to discard and re-render client-side starting from the **nearest Suspense boundary**, not the whole tree and not just the single mismatched node. Suspense placement therefore directly bounds mismatch blast radius, independent of streaming concerns. - Next.js's own streaming guide demonstrates wrapping **independent** async sections in **separate, sibling** `<Suspense>` boundaries so each streams in as its own data resolves, rather than one boundary gating the whole page behind its slowest child. - Streaming can be implemented via a route's `loading.js` file or via explicit `<Suspense>` around a component. For bots/crawlers, Next.js waits for all data fetching to finish and sends the fully rendered page rather than streaming progressively — a bot-facing request does not get the same partial-render exposure window a browser request does, which matters when reasoning about what a "premature" stream can leak to which caller. ## Non-negotiable design rules ### 1. Every suspending read needs a paired Error Boundary Do not approve a Suspense boundary wrapping a `use()` call, a suspending fetch, or any Suspense-triggering data read without confirming an Error Boundary exists somewhere in its ancestor chain. If the user has not added one, this is a defect: on rejection, the user sees an unhandled crash/blank instead of a fallback message. ### 2. Boundary granularity should match content speed, not code convenience One `<Suspense>` around an entire page is the easiest thing to write and the worst thing to ship when the page mixes fast content (e.g., a nav shell) with slow content (e.g., a report requiring heavy aggregation). The fast content is held hostage by the slow content's fallback. Prefer sibling boundaries per independently-loading section, following the pattern Next.js documents for parallel streaming. ### 3. Boundary granularity also bounds mismatch blast radius A too-coarse boundary is a double defect: it blocks fast content behind slow content *and* it means any hydration mismatch inside that huge boundary forces React to discard and re-render the entire boundary's subtree client-side, not just the small mismatched region. Narrower, purpose-scoped boundaries reduce both problems simultaneously. ### 4. A Suspense fallback is not a substitute for a loading-state design review Confirm the fallback communicates something coherent (skeleton matching the eventual layout, or a clear loading indicator) rather than an empty or jarring placeholder — a correct boundary with a poor fallback is still a shippable defect, just a lower-severity one than a missing Error Boundary. ## Minimal safe review flow 1. Map every `<Suspense>` boundary in the tree under review and what it wraps. 2. For each boundary, identify what suspends inside it (a `use()` call, a Server Component awaiting a fetch, a lazy-loaded component) and confirm an Error Boundary exists in its ancestor chain — if not, that is a defect (see `fetch-waterfall-and-auth-timing.md`'s parent skill rule: crash-to-blank is not acceptable). 3. For each boundary, ask whether it wraps content of meaningfully different load-latency profiles. If yes, and content is independent, recommend splitting into sibling boundaries per the Next.js parallel-streaming pattern. 4. For each boundary wrapping content that depends on a prior async result (a genuinely sequential dependency, e.g., needing an ID from a previous fetch before the next component can render), confirm the nesting reflects that dependency rather than an accidental structure that could be flattened. 5. Confirm the fallback UI is coherent, not a placeholder afterthought. 6. State the verdict per boundary, not just for the tree as a whole — a tree can be correct in three boundaries and defective in one. ## High-risk assumptions to kill - "It's wrapped in Suspense, so errors are handled" — Suspense handles pending state, not rejection. - "One Suspense boundary at the top is simpler and streaming still works" — technically true, but it defeats the purpose of granular streaming and maximizes mismatch blast radius. - "The fallback shows briefly so its content doesn't matter" — a jarring or mismatched-size fallback causes layout shift on resolve, which is a measurable UX/performance regression, not a cosmetic detail. - "Bots see the same partial-render window a browser does" — they do not; Next.js waits for full data resolution before responding to bots/crawlers, which changes threat/exposure reasoning for that path specifically. ## When to push back Push back if the user asks to: - remove a Suspense boundary "to make the loading flicker go away" without addressing the underlying slow fetch, - wrap a suspending, rejectable data read in Suspense with explicit instruction to skip the Error Boundary "for now," - collapse several independently-loading sections into one shared boundary purely to reduce the number of `<Suspense>` tags in the file. Those trade a visible symptom for a worse, less visible one (either a crash-to-blank on failure, or slow content gating fast content).
-
-
metadata.json 1.5 KB
{ "id": "ssr-hydration-streaming-diagnosis", "name": "SSR Hydration & Streaming Diagnosis", "type": "skill", "provider": "frontend", "harnesses": [ "claude-code", "cursor", "codex", "gemini", "kiro", "other" ], "summary": "Diagnoses hydration-mismatch errors and streaming/Suspense-boundary placement issues to their root cause, using framework-version-specific evidence rather than guesswork, with root-cause-before-fix and auth-before-stream as hard gates.", "source_type": "original", "official_docs": [ "https://react.dev/reference/react-dom/client/hydrateRoot", "https://react.dev/reference/react/Suspense", "https://react.dev/reference/react/use", "https://nextjs.org/docs/app/building-your-application/rendering/server-components", "https://web.dev/articles/optimize-lcp" ], "security_notes": "Never accept suppressHydrationWarning as the fix without a documented, unavoidable non-determinism justification; treat it as a rejected finding when used without an identified root cause. Treat streaming that flushes page structure or content before an authorization check resolves as a potential information-disclosure finding requiring escalation, not a performance note. Static-review-only skill: it reads and greps component/boundary source and reported error text but never executes, builds, or runs application code.", "last_verified": "2026-07-02", "path": "skills/frontend/ssr-hydration-streaming-diagnosis", "author": "github: VincentChuWaiChow", "version": "0.1.0" } -
SKILL.md 7.6 KB
--- name: ssr-hydration-streaming-diagnosis description: Diagnoses hydration-mismatch errors and streaming/Suspense-boundary structural issues to their specific root cause — non-deterministic rendering source, missing error boundary, serial data-fetch waterfall, or premature auth-unchecked streaming — grounded in the exact React/Next.js version's diagnostic behavior. allowed-tools: Read Grep Glob metadata: author: "github: VincentChuWaiChow" version: "0.1.0" updated: "2026-07-02" category: observability --- # SSR Hydration & Streaming Diagnosis ## Purpose Diagnose hydration-mismatch errors and streaming/Suspense-boundary structural issues to their specific root cause, without collapsing every hydration warning into a reflex `suppressHydrationWarning` fix or every streaming complaint into "wrap it in Suspense." This skill exists so root-cause identification — non-deterministic rendering source, missing error boundary, serial fetch waterfall, or auth-unchecked premature streaming — happens before any fix is proposed, and so that fix proposal never substitutes for diagnosis. ## When to use Use this skill when the user asks to: - diagnose a hydration-mismatch error or console warning (React 18 warning-style or React 19 diff-style), - investigate a TTFB/LCP regression that followed an SSR or streaming change, - review why a Suspense-wrapped section crashes to blank instead of degrading gracefully, - review a new Suspense/error-boundary tree design before it ships. Do not use this skill for: - state/store serialization design decisions with no specific mismatch or streaming symptom — that is `state-management-decision-review`, - route-tree or code-splitting review unrelated to SSR/streaming mechanics — that is `routing-navigation-review`, - generic component decomposition or state-placement review — that is `react-component-architecture-review`. ## Context7 Documentation Protocol - Resolve `/reactjs/react.dev` before diagnosing any hydration-mismatch error. The diagnostic error *format itself* is version-specific: React 19 emits a single detailed diff-style error (`Hydration failed because the server rendered HTML didn't match the client...` with a `+ Client` / `- Server` diff), while React 18 emits separate, less-specific warnings and silently "patches" mismatched nodes instead of remounting from the nearest Suspense boundary. Diagnosing a React 18 warning against the React 19 mental model (or vice versa) produces a wrong root-cause guess. - Before recommending a fix, confirm the installed React major version (check `package.json` / lockfile) and call `query-docs` scoped to that version for "hydration mismatch" and, if `use()` or a Suspense-wrapped fetch is involved, for "use hook Suspense error boundary." - Resolve `/vercel/next.js` and call `query-docs` scoped to the confirmed Next.js version before asserting streaming/Suspense-boundary semantics, `loading.js` behavior, or bot/crawler streaming exceptions — Next.js waits for all data fetching to finish before sending a fully rendered page to bots/crawlers instead of streaming progressively, which changes what "premature streaming" even means for that request path. - If Context7 is unavailable, fall back to the `official_docs` URLs in this skill's `metadata.json` and label every claim `documentation-based, unverified against current release`. ## Lean operating rules - Root cause before fix, always. Do not propose `suppressHydrationWarning`, a Suspense-boundary restructure, or a caching change until the specific mechanism is named and evidenced. A fix proposed before root cause is identified is a guess wearing a diagnosis costume. - The four canonical hydration-mismatch causes per React's own diagnostic message are: a server/client branch (`typeof window !== 'undefined'`), variable input (`Date.now()`, `Math.random()`), locale-dependent formatting that differs between server and client, and external/changing data rendered without a matching snapshot sent alongside the HTML — plus invalid HTML tag nesting as a fifth, structural cause. Map the reported mismatch to one of these five before naming a fix. - `suppressHydrationWarning` is a one-level-deep escape hatch, not a fix. It is acceptable only when the root cause is a genuinely unavoidable non-determinism (for example, a legitimately locale/timezone-dependent timestamp) and only with a documented justification comment at the call site. Treat any use of it without an identified root cause and written justification as a rejected finding, not an approved one. - Since React 18, a hydration mismatch from missing/extra text content is treated as an error, not a soft warning: React discards and re-renders client-side from the nearest `<Suspense>` boundary rather than patching individual nodes. This means the blast radius of a mismatch is bounded by Suspense placement — a missing or too-coarse Suspense boundary turns a small mismatch into a large client re-render. - Every component that suspends via `use()` on a Promise (or any Suspense-triggering data read) must have a paired Error Boundary somewhere in its ancestor tree, because a rejected Promise propagates to the nearest Error Boundary, not the nearest Suspense fallback. A Suspense boundary with no ancestor Error Boundary is a crash-to-blank defect, not a style preference. - Suspense boundaries that are too coarse (one boundary wrapping an entire page, mixing fast and slow content) block fast content behind the page's slowest fetch. Prefer sibling Suspense boundaries around independently-loading sections so each streams in as its own data resolves, per Next.js's own parallel-streaming pattern. - A fetch that depends on the result of a prior fetch (for example, fetching playlists that require an artist ID from a preceding fetch) is legitimately sequential — do not flag that as a waterfall defect. Only flag fetches that are data-independent but are still awaited one after another instead of started concurrently. - Streaming that flushes page structure or partial content to the client before an authorization check has resolved is a potential information-disclosure defect, not a performance nitpick — escalate it, do not file it as a UX note. - Never execute, build, or run application code as part of this review; this is a static-review skill (Read/Grep/Glob only) — diagnose from the reported error text, the component/boundary source, and any network/fetch-order trace the user provides. ## References Load these only when needed: - [Hydration mismatch diagnosis](references/hydration-mismatch-diagnosis.md) — use when diagnosing a specific hydration-mismatch error or warning to its root cause. - [Suspense and streaming structure review](references/suspense-streaming-structure.md) — use when reviewing or designing a Suspense/error-boundary tree, or diagnosing a TTFB/LCP regression or crash-to-blank symptom. - [Fetch waterfall and auth-timing review](references/fetch-waterfall-and-auth-timing.md) — use when diagnosing a sequential-fetch performance issue or reviewing whether streaming exposes content before authorization resolves. ## Response minimum Return, at minimum: - the specific root-cause mechanism identified (or explicit statement that root cause is unresolved and more evidence is needed — never a guess presented as a diagnosis), - the React/Next.js major version the diagnosis was verified against, - evidence level (`documentation-based`, `repo evidence`, `user-provided evidence`, or `inference`) and what Context7 query grounded the claim, - the proposed fix scoped to the identified root cause, or explicit refusal to propose a fix until root cause is confirmed, - security/disclosure caveat if streaming-before-auth risk is present, - verdict: approve / approve-with-notes / block.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.