Skip to content

Conversation

@mattcosta7
Copy link
Contributor

@mattcosta7 mattcosta7 commented Feb 2, 2026

Relates to #7493

Overview

This PR refactors the useMedia hook to use React's useSyncExternalStore API, 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 + useEffect to track media query changes. This pattern can cause hydration mismatches in SSR scenarios because:

  1. The initial state is computed during render, which may differ between server and client
  2. The useEffect callback runs after hydration, potentially causing a flash of incorrect content

Why canUseDOM in useState causes hydration issues

The root cause is how the useState initializer interacts with canUseDOM:

const [matches, setMatches] = React.useState(() => {
  // ...
  if (canUseDOM) {
    return window.matchMedia(mediaQueryString).matches
  }
  return false
})

The useState initializer function runs during the initial render on both server and client:

  • Server-side render: canUseDOM is false (no window exists) → state initializes to false
  • Client-side hydration: canUseDOM is true → state initializes to window.matchMedia(mediaQueryString).matches

If the user's actual media query result is true (e.g., they have a coarse pointer, or their viewport is narrow), the client computes true while the server rendered false. 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 defaultState is provided, the subsequent useEffect that syncs state with matchMedia runs after hydration, which can still cause a visible flash of incorrect content.

Solution

The new implementation:

  • Uses useSyncExternalStore which React specifically designed for subscribing to external data sources with SSR support
  • Provides a getServerSnapshot callback that returns the defaultState (or false with a warning) during server rendering
  • React automatically uses getServerSnapshot during hydration to ensure consistency
  • Simplifies the logic by separating context-based overrides from the actual media query subscription

Changelog

New

  • Introduced internal useMediaQuery helper that uses useSyncExternalStore for hydration-safe media query subscriptions

Changed

  • useMedia now delegates to useSyncExternalStore instead of useState + useEffect
  • Updated test mock to use a getter for matches property to properly support useSyncExternalStore's snapshot pattern

Removed

  • Removed manual state synchronization logic that was prone to hydration issues

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

This is a bug fix for hydration mismatches with no API changes to consumers.

Testing & Reviewing

  • Existing tests have been updated to work with useSyncExternalStore
  • The mock now tracks currentMatch state and uses a getter so that useSyncExternalStore can correctly read the current value via getSnapshot
  • Test SSR scenarios in Next.js examples to verify no hydration warnings appear

Merge checklist

@mattcosta7 mattcosta7 self-assigned this Feb 2, 2026
@changeset-bot
Copy link

changeset-bot bot commented Feb 2, 2026

🦋 Changeset detected

Latest commit: 6d91be6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

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

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Feb 2, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2026

👋 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 integration-tests: skipped manually label to skip these checks.

@mattcosta7 mattcosta7 changed the title hydration safe useMedia ensure useMedia avoids hydration issues Feb 2, 2026
Copy link
Contributor

Copilot AI left a 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/useEffect pattern with useSyncExternalStore for safe external store subscription
  • Introduced internal useMediaQuery helper 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

@primer-integration
Copy link

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/12400

@primer-integration
Copy link

Integration test results from github/github-ui:

Passed  CI   Passed
Passed  VRT   Passed
Passed  Projects   Passed

All checks passed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants