-
Notifications
You must be signed in to change notification settings - Fork 648
ensure useMedia avoids hydration issues
#7494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 6d91be6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Or, apply the |
useMedia avoids hydration issues
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR refactors the useMedia hook to use React's useSyncExternalStore API to prevent hydration mismatches in server-side rendering scenarios. The previous implementation using useState + useEffect could cause inconsistencies between server-rendered content and client hydration when media query results differed between environments.
Changes:
- Replaced
useState/useEffectpattern withuseSyncExternalStorefor safe external store subscription - Introduced internal
useMediaQueryhelper function that properly separates media query logic from context overrides - Updated test mocks to use property getters to work correctly with
useSyncExternalStore's snapshot mechanism
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/react/src/hooks/useMedia.tsx | Refactored to use useSyncExternalStore with proper server/client snapshot handling for hydration safety |
| packages/react/src/hooks/tests/useMedia.test.tsx | Updated mock to track state and use getter for matches property to support snapshot-based subscriptions |
| package-lock.json | Version bump to 38.9.0 for the patch release |
| .changeset/smooth-ladybugs-hunt.md | Added changeset documenting the patch fix |
|
👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/12400 |
Relates to #7493
Overview
This PR refactors the
useMediahook to use React'suseSyncExternalStoreAPI, which is specifically designed to safely subscribe to external stores (like browser media queries) while avoiding hydration mismatches between server and client rendering.This is mostly defense in depth + gets rid of a post hydration render cycle when we useMedia with a default value - after hydration it's fine to intiialize with the query - useSyncExternalstore will handle this well for us and avoid an extra render pass
Problem
The previous implementation used
useState+useEffectto track media query changes. This pattern can cause hydration mismatches in SSR scenarios because:useEffectcallback runs after hydration, potentially causing a flash of incorrect contentWhy
canUseDOMinuseStatecauses hydration issuesThe root cause is how the
useStateinitializer interacts withcanUseDOM:The
useStateinitializer function runs during the initial render on both server and client:canUseDOMisfalse(nowindowexists) → state initializes tofalsecanUseDOMistrue→ state initializes towindow.matchMedia(mediaQueryString).matchesIf the user's actual media query result is
true(e.g., they have a coarse pointer, or their viewport is narrow), the client computestruewhile the server renderedfalse. React detects this mismatch during hydration because the initial client render doesn't match the server-rendered HTML, resulting in a hydration error.Even when
defaultStateis provided, the subsequentuseEffectthat syncs state withmatchMediaruns after hydration, which can still cause a visible flash of incorrect content.Solution
The new implementation:
useSyncExternalStorewhich React specifically designed for subscribing to external data sources with SSR supportgetServerSnapshotcallback that returns thedefaultState(orfalsewith a warning) during server renderinggetServerSnapshotduring hydration to ensure consistencyChangelog
New
useMediaQueryhelper that usesuseSyncExternalStorefor hydration-safe media query subscriptionsChanged
useMedianow delegates touseSyncExternalStoreinstead ofuseState+useEffectmatchesproperty to properly supportuseSyncExternalStore's snapshot patternRemoved
Rollout strategy
This is a bug fix for hydration mismatches with no API changes to consumers.
Testing & Reviewing
useSyncExternalStorecurrentMatchstate and uses a getter so thatuseSyncExternalStorecan correctly read the current value viagetSnapshotMerge checklist