Skip to content

Commit

Permalink
[@mantine/hooks] use-media-query: Add initial value support (#1244)
Browse files Browse the repository at this point in the history
* Add default value to use-media-query for ssr

* [@mantine/hooks] use-media-query: Update docs to show usage for ssr

* [@mantine/hooks] use-media-query: Replace SSR demo with code sample

* [@mantine/hooks] use-media-query: Run prettier:write
  • Loading branch information
teaguestockwell committed Apr 15, 2022
1 parent 8516a5c commit 6251998
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 4 deletions.
24 changes: 23 additions & 1 deletion docs/src/docs/hooks/use-media-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,31 @@ use-media-query hook allows to subscribe to media queries.
It receives media query as an argument and returns true
if given media query matches current state.
Hook relies on `window.matchMedia()` [API](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)
and will always return false if api is not available (e.g. during server side rendering).
and will return false if api is not available.

Hook takes media query as first argument and returns true if query is satisfied.
Resize browser window to trigger `window.matchMedia` event:

<Demo data={HooksDemos.useMediaQueryDemo} />

## Server Side Rendering
When rendering a component on the server that uses useMediaQuery, it is important to pass the second argument `initialValue` because without it the server may generate html that the React client cannot correctly hydrate.
See the [React docs](https://reactjs.org/docs/react-dom.html#hydrate) for more on why this is can lead to costly bugs.

For example, the server will generate the html for clients using a smaller device.
After the React client hydrates this, the hook will render again and return the result of the media query:

```tsx
import { Badge } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';

function Demo() {
const matches = useMediaQuery('(min-width: 900px)', false);

return (
<Badge color={matches ? 'teal' : 'red'} variant="filled">
Breakpoint {matches ? 'matches' : 'does not match'}
</Badge>
);
}`
```
42 changes: 42 additions & 0 deletions src/mantine-hooks/src/use-media-query/use-media-query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { renderHook } from '@testing-library/react-hooks';
import { renderHook as renderHookSSR } from '@testing-library/react-hooks/server';
import { useMediaQuery } from './use-media-query';

describe('@mantine/hooks/use-media-query', () => {
beforeEach(() => {
const mediaMatches = {
'(min-width: 500px)': true,
'(min-width: 1000px)': false,
};
window.matchMedia = (query) =>
({
matches: mediaMatches[query] ?? false,
addListener: jest.fn(),
removeListener: jest.fn(),
} as any);
});
it('should return true if media query matches', () => {
const { result } = renderHook(() => useMediaQuery('(min-width: 500px)'));
expect(result.current).toBe(true);
});
it('should return false if media query does not match', () => {
const { result } = renderHook(() => useMediaQuery('(min-width: 1200px)'));
expect(result.current).toBe(false);
});
it('should return default state before hydration', () => {
const { result } = renderHookSSR(() => useMediaQuery('(min-width: 500px)', false));
expect(result.current).toBe(false);
});
it('should return media query result after hydration 500px', async () => {
const { result, hydrate } = renderHookSSR(() => useMediaQuery('(min-width: 500px)', false));
expect(result.current).toBe(false);
hydrate();
expect(result.current).toBe(true);
});
it('should return media query result after hydration 1200px', async () => {
const { result, hydrate } = renderHookSSR(() => useMediaQuery('(min-width: 1200px)', true));
expect(result.current).toBe(true);
hydrate();
expect(result.current).toBe(false);
});
});
16 changes: 13 additions & 3 deletions src/mantine-hooks/src/use-media-query/use-media-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,25 @@ function attachMediaListener(query: MediaQueryList, callback: MediaQueryCallback
}
}

function getInitialValue(query: string) {
function getInitialValue(query: string, initialValue?: boolean) {
if (initialValue !== undefined) {
return initialValue;
}

if (typeof window !== 'undefined' && 'matchMedia' in window) {
return window.matchMedia(query).matches;
}

// eslint-disable-next-line no-console
console.error(
'[@mantine/hooks] use-media-query: Please provide a default value when using server side rendering to prevent a hydration mismatch.'
);

return false;
}

export function useMediaQuery(query: string) {
const [matches, setMatches] = useState(getInitialValue(query));
export function useMediaQuery(query: string, initialValue?: boolean) {
const [matches, setMatches] = useState(getInitialValue(query, initialValue));
const queryRef = useRef<MediaQueryList>();

// eslint-disable-next-line consistent-return
Expand Down

0 comments on commit 6251998

Please sign in to comment.