fix: hydration mismatch — defer showHeroCanvas matchMedia to useEffect#104
Merged
Conversation
React error #418 surfaced in production after PR #103 shipped: the HomePage's `showHeroCanvas` useState initialiser branched on window.matchMedia at first render. On SSR it returned false (typeof window === 'undefined' guard); on the client first render (during hydration) it returned !matches — true on desktop. The conditional {showHeroCanvas && <HeroBackground />} then differed between the prerendered HTML and the hydrating client → React detected a tree mismatch at the subtree, blew up the whole hydration, and re-rendered the entire root from scratch on the client. Two-pass pattern fix: initial state is `false` on both SSR and the first client render, so hydration sees the same tree shape on both sides. After hydration the effect reads matchMedia and flips the flag — a normal post-mount re-render mounts HeroBackground on desktop. Brief flash of "no background" is invisible because the canvas is decorative and the hero content paints from the prerendered HTML regardless. Same pattern as the ThemeProvider fix in the SSR PR — anything that reads window / matchMedia / localStorage in a useState initialiser during render is an SSR/client mismatch waiting to happen. The useState default must match what SSR produces; the real value arrives in a mount effect. Why this only surfaced now: the navbar PR #103 happened to expose it because the navbar's ResizeObserver + useLayoutEffect path triggered extra hydration scrutiny in React 19's dev / error-recovery flow. The bug was latent since PR #94 (lazy HeroBackground); React was silently recovering and re-rendering the root before, but it counts as an error now.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
React error #418 surfaced in production after the navbar PR #103 landed. Cause: `HomePage.tsx`'s `showHeroCanvas` `useState` initialiser branched on `window.matchMedia` at first render — `false` on SSR, `true` on a desktop client. The conditional `{showHeroCanvas && }` then differed between the prerendered HTML and the hydrating client, and React blew up the hydration at that subtree.
Fix
Two-pass pattern: initial state is `false` on both SSR and the first client render, so hydration sees the same tree shape on both sides. After hydration the effect reads matchMedia and flips the flag — a normal post-mount re-render mounts HeroBackground on desktop.
Brief flash of "no background" on desktop is invisible because the canvas is decorative and the hero content paints from the prerendered HTML regardless.
Same pattern as the `ThemeProvider` fix in PR #95 (the SSR PR) — anything that reads `window` / `matchMedia` / `localStorage` in a useState initialiser during render is an SSR/client mismatch waiting to happen.
Why now
The bug was latent since PR #94 (lazy HeroBackground). React 19 was silently recovering and re-rendering the root before. PR #103's navbar `useLayoutEffect` + ResizeObserver path triggered extra hydration scrutiny that promoted the silent recovery to a visible error.
Test plan