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 1 commit
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
4 changes: 3 additions & 1 deletion docs/src/docs/hooks/use-media-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ 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.
When server side rendering, it is important to pass ssrInitialValue because without it the server's initial state will fallback to false, but the client will initialize to the result of the media query.
When React hydrates the server render, it may not match the client's state. See the [React docs](https://reactjs.org/docs/react-dom.html#hydrate) for more on why this is can lead to costly bugs 🐛.

Hook takes media query as first argument and returns true if query is satisfied.
Resize browser window to trigger `window.matchMedia` event:
Expand Down
41 changes: 41 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,41 @@
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);
});
});
18 changes: 15 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,27 @@ function attachMediaListener(query: MediaQueryList, callback: MediaQueryCallback
}
}

function getInitialValue(query: string) {
function getInitialValue(query: string, ssrInitialValue?: boolean) {
// server side render must have a default value to prevent a React hydration mismatch
if (ssrInitialValue !== undefined) {
return ssrInitialValue;
}

// client side render
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, ssrInitialValue?: boolean) {
const [matches, setMatches] = useState(getInitialValue(query, ssrInitialValue));
const queryRef = useRef<MediaQueryList>();

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