Skip to content
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

Add default value to use-media-query for SSR #1244

Merged
merged 4 commits into from
Apr 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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