Skip to content

Feat/use media query - #19

Merged
thepuskar merged 6 commits into
mainfrom
feat/use-media-query
May 19, 2026
Merged

Feat/use media query#19
thepuskar merged 6 commits into
mainfrom
feat/use-media-query

Conversation

@thepuskar

@thepuskar thepuskar commented May 13, 2026

Copy link
Copy Markdown
Owner

Summary

What does this PR change?

This PR adds a production-ready useMediaQuery hook family:

  • useMediaQuery(query, options)
  • useMediaQueries(queries, options)
  • useBreakpoint(options)

It includes SSR/CSR-safe implementation using useSyncExternalStore, shared matchMedia subscriptions, legacy listener fallback, dynamic query support, TypeScript types, barrel exports, docs, live docs demo, and tests for SSR, hydration, cleanup, fallback behavior, StrictMode, multi-query usage, and breakpoints.

Why is it needed?

Apps often need responsive state in React components without manually wiring window.matchMedia. This gives the package a reliable, typed, Next.js-friendly media query API that prevents hydration mismatches, avoids duplicate listeners across large apps, cleans up safely, and works in unsupported or server environments with predictable fallback values.

Checklist

  • Tests added/updated
  • npm run lint passes
  • npm run typecheck passes
  • npm run test passes
  • npm run build passes
  • Docs/README updated if API changed

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying use-kit with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3e7613f
Status: ✅  Deploy successful!
Preview URL: https://3d86d4a1.use-kit.pages.dev
Branch Preview URL: https://feat-use-media-query.use-kit.pages.dev

View logs

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a suite of responsive hooks—useMediaQuery, useMediaQueries, and useBreakpoint—along with comprehensive documentation, a demo component, and unit/SSR tests. The implementation leverages useSyncExternalStore for concurrent-safe rendering and shares matchMedia listeners to optimize performance. Feedback focused on optimizing the calculation of query signatures using useMemo to avoid redundant stringification and addressing the fragility of breakpoint detection logic that currently relies on object key insertion order.

Comment on lines +45 to +58
function getActiveBreakpoint<TBreakpoints extends BreakpointMap>(
entries: BreakpointEntries<TBreakpoints>,
matches: BreakpointMatches<TBreakpoints>,
): BreakpointName<TBreakpoints> | null {
let activeBreakpoint: BreakpointName<TBreakpoints> | null = null;

entries.forEach(([key]) => {
if (matches[key]) {
activeBreakpoint = key;
}
});

return activeBreakpoint;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getActiveBreakpoint function identifies the active breakpoint by iterating through the entries and returning the last one that matches. This logic correctly finds the "largest" matching breakpoint only if the input object is ordered from smallest to largest width (e.g., sm, md, lg).

While the documentation mentions this requirement, relying on object key insertion order can be fragile in JavaScript. If a user provides a custom breakpoint map with out-of-order keys, the hook will return incorrect results. Consider adding a runtime check to ensure the provided breakpoints are in ascending order, or explicitly sorting the entries by their numeric values before processing.

Comment on lines +71 to +72
const entries = getBreakpointEntries(breakpointMap);
const signature = getBreakpointSignature(entries);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The signature is calculated on every render, which involves JSON.stringify on the breakpoint entries. While the overhead is small for typical breakpoint maps, it can be avoided when the breakpointMap reference is stable.

Wrapping this in useMemo ensures that the stringification only happens when the object reference changes, while still maintaining the deep-equality check for inline objects.

Suggested change
const entries = getBreakpointEntries(breakpointMap);
const signature = getBreakpointSignature(entries);
const signature = useMemo(() => getBreakpointSignature(getBreakpointEntries(breakpointMap)), [breakpointMap]);

Comment on lines +113 to +114
const entries = getMediaQueryEntries(queries);
const signature = getMediaQuerySignature(entries);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The signature is calculated using JSON.stringify on every render because getMediaQueryEntries returns a new array reference each time. While this correctly handles deep equality for inline objects, it adds unnecessary overhead when the queries object is stable.

Wrapping the signature calculation in useMemo avoids the stringification cost for stable objects while still providing the deep-equality benefit for inline ones.

Suggested change
const entries = getMediaQueryEntries(queries);
const signature = getMediaQuerySignature(entries);
const signature = useMemo(() => getMediaQuerySignature(getMediaQueryEntries(queries)), [queries]);

@thepuskar
thepuskar merged commit 4a10688 into main May 19, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant