Feat/use media query - #19
Conversation
Deploying use-kit with
|
| 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 |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| const entries = getBreakpointEntries(breakpointMap); | ||
| const signature = getBreakpointSignature(entries); |
There was a problem hiding this comment.
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.
| const entries = getBreakpointEntries(breakpointMap); | |
| const signature = getBreakpointSignature(entries); | |
| const signature = useMemo(() => getBreakpointSignature(getBreakpointEntries(breakpointMap)), [breakpointMap]); |
| const entries = getMediaQueryEntries(queries); | ||
| const signature = getMediaQuerySignature(entries); |
There was a problem hiding this comment.
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.
| const entries = getMediaQueryEntries(queries); | |
| const signature = getMediaQuerySignature(entries); | |
| const signature = useMemo(() => getMediaQuerySignature(getMediaQueryEntries(queries)), [queries]); |
Summary
What does this PR change?
This PR adds a production-ready
useMediaQueryhook family:useMediaQuery(query, options)useMediaQueries(queries, options)useBreakpoint(options)It includes SSR/CSR-safe implementation using
useSyncExternalStore, sharedmatchMediasubscriptions, 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
npm run lintpassesnpm run typecheckpassesnpm run testpassesnpm run buildpasses