Skip to content

Commit

Permalink
fix: useMedia SSR hydration bug with defaultState (#2216)
Browse files Browse the repository at this point in the history
* fix: useMedia SSR hydration bug with defaultState

Prevent a React hydration mismatch when a default value is provided
by not defaulting to window.matchMedia(query).matches.

* Refactor nested ifs
  • Loading branch information
teaguestockwell committed Dec 30, 2021
1 parent 46b401f commit 5c01189
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 4 deletions.
2 changes: 2 additions & 0 deletions docs/useMedia.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ useMedia(query: string, defaultState: boolean = false): boolean;
```

The `defaultState` parameter is only used as a fallback for server side rendering.

When server side rendering, it is important to set this parameter 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 🐛.
26 changes: 22 additions & 4 deletions src/useMedia.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { useEffect, useState } from 'react';
import { isBrowser } from './misc/util';

const useMedia = (query: string, defaultState: boolean = false) => {
const [state, setState] = useState(
isBrowser ? () => window.matchMedia(query).matches : defaultState
);
const getInitialState = (query: string, defaultState?: boolean) => {
// Prevent a React hydration mismatch when a default value is provided by not defaulting to window.matchMedia(query).matches.
if (defaultState !== undefined) {
return defaultState;
}

if (isBrowser) {
return window.matchMedia(query).matches;
}

// A default value has not been provided, and you are rendering on the server, warn of a possible hydration mismatch when defaulting to false.
if (process.env.NODE_ENV !== 'production') {
console.warn(
'`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatches.'
);
}

return false;
};

const useMedia = (query: string, defaultState?: boolean) => {
const [state, setState] = useState(getInitialState(query, defaultState));

useEffect(() => {
let mounted = true;
Expand Down
42 changes: 42 additions & 0 deletions tests/useMedia.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 { useMedia } from '../src';

const createMockMediaMatcher = (matches: Record<string, boolean>) => (qs: string) => ({
matches: matches[qs] ?? false,
addListener: () => {},
removeListener: () => {},
});

describe('useMedia', () => {
beforeEach(() => {
window.matchMedia = createMockMediaMatcher({
'(min-width: 500px)': true,
'(min-width: 1000px)': false,
}) as any;
});
it('should return true if media query matches', () => {
const { result } = renderHook(() => useMedia('(min-width: 500px)'));
expect(result.current).toBe(true);
});
it('should return false if media query does not match', () => {
const { result } = renderHook(() => useMedia('(min-width: 1200px)'));
expect(result.current).toBe(false);
});
it('should return default state before hydration', () => {
const { result } = renderHookSSR(() => useMedia('(min-width: 500px)', false));
expect(result.current).toBe(false);
});
it('should return media query result after hydration', async () => {
const { result, hydrate } = renderHookSSR(() => useMedia('(min-width: 500px)', false));
expect(result.current).toBe(false);
hydrate();
expect(result.current).toBe(true);
});
it('should return media query result after hydration', async () => {
const { result, hydrate } = renderHookSSR(() => useMedia('(min-width: 1200px)', true));
expect(result.current).toBe(true);
hydrate();
expect(result.current).toBe(false);
});
});

0 comments on commit 5c01189

Please sign in to comment.