Skip to content

Commit 087b2b1

Browse files
authored
fix(useMediaQuery): add option to martch media query on first render (#1020)
* fix(useMediaQuery): Return boolean describing media query match on first render instead of undefined This gets rid of the value changing from undefined to boolean on the first renders, which might trigger undesired side-effects for users. BREAKING CHANGE: `useMediaQuery` and `useScreenOrientation` now returns matched media query state on first render by default, SSR users can change that behaviour via hook options. fix #1000
1 parent add827e commit 087b2b1

File tree

8 files changed

+99
-39
lines changed

8 files changed

+99
-39
lines changed

src/useMediaQuery/__docs__/story.mdx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Canvas, Meta, Story } from '@storybook/addon-docs';
2-
import { Example } from './example.stories';
3-
import { ImportPath } from '../../__docs__/ImportPath';
1+
import {Canvas, Meta, Story} from '@storybook/addon-docs';
2+
import {Example} from './example.stories';
3+
import {ImportPath} from '../../__docs__/ImportPath';
44

55
<Meta title="Sensor/useMediaQuery" component={Example} />
66

@@ -22,7 +22,11 @@ Tracks the state of CSS media query.
2222
## Reference
2323

2424
```ts
25-
export function useMediaQuery(query: string): boolean | undefined;
25+
interface UseMediaQueryOptions {
26+
initializeWithValue?: boolean;
27+
}
28+
29+
export function useMediaQuery(query: string, options?: UseMediaQueryOptions): boolean | undefined;
2630
```
2731

2832
#### Importing
@@ -32,8 +36,12 @@ export function useMediaQuery(query: string): boolean | undefined;
3236
#### Arguments
3337

3438
- **query** _`string`_ - CSS media query to track.
39+
- **options** _`object`_ - Hook options:
40+
- **initializeWithValue** _`boolean`_ _(default: true)_ - Determine media query match state on first
41+
render. Setting this to false will make the hook yield `undefined` on first render. We suggest
42+
setting this to `false` during SSR.
3543

3644
#### Return
3745

38-
`boolean` - `true` in case document matches media query, `false` otherwise.
39-
`undefined` - initially, when media query isn't matched yet.
46+
`boolean` - `true` if document matches the media query, `false` if not. `undefined` on first render
47+
if `initializeWithValue` was set to `false`.

src/useMediaQuery/__tests__/dom.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,23 @@ describe('useMediaQuery', () => {
5454
expect(result.error).toBeUndefined();
5555
});
5656

57-
it('should return undefined on first render', () => {
58-
const { result } = renderHook(() => useMediaQuery('max-width : 768px'));
57+
it('should return undefined on first render, if initializeWithValue is false', () => {
58+
const { result } = renderHook(() =>
59+
useMediaQuery('max-width : 768px', { initializeWithValue: false })
60+
);
5961
expect(result.all.length).toBe(2);
6062
expect(result.all[0]).toBe(undefined);
6163
expect(result.current).toBe(false);
6264
});
6365

66+
it('should return value on first render, if initializeWithValue is true', () => {
67+
const { result } = renderHook(() =>
68+
useMediaQuery('max-width : 768px', { initializeWithValue: true })
69+
);
70+
expect(result.all.length).toBe(1);
71+
expect(result.current).toBe(false);
72+
});
73+
6474
it('should return match state', () => {
6575
const { result } = renderHook(() => useMediaQuery('max-width : 768px'));
6676
expect(result.current).toBe(false);

src/useMediaQuery/__tests__/ssr.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ describe('useMediaQuery', () => {
77
});
88

99
it('should render', () => {
10-
const { result } = renderHook(() => useMediaQuery('max-width : 768px'));
10+
const { result } = renderHook(() =>
11+
useMediaQuery('max-width : 768px', { initializeWithValue: false })
12+
);
1113
expect(result.error).toBeUndefined();
1214
});
1315

14-
it('should return undefined on first render', () => {
15-
const { result } = renderHook(() => useMediaQuery('max-width : 768px'));
16+
it('should return undefined on first render, if initializeWithValue is set to false', () => {
17+
const { result } = renderHook(() =>
18+
useMediaQuery('max-width : 768px', { initializeWithValue: false })
19+
);
1620
expect(result.current).toBeUndefined();
1721
});
1822
});

src/useMediaQuery/useMediaQuery.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { Dispatch, useEffect } from 'react';
2-
import { useSafeState } from '../useSafeState/useSafeState';
1+
import { Dispatch, useEffect, useState } from 'react';
32

43
const queriesMap = new Map<
54
string,
@@ -8,24 +7,28 @@ const queriesMap = new Map<
87

98
type QueryStateSetter = (matches: boolean) => void;
109

10+
const createQueryEntry = (query: string) => {
11+
const mql = matchMedia(query);
12+
const dispatchers = new Set<QueryStateSetter>();
13+
const listener = () => {
14+
dispatchers.forEach((d) => d(mql.matches));
15+
};
16+
17+
if (mql.addEventListener) mql.addEventListener('change', listener, { passive: true });
18+
else mql.addListener(listener);
19+
20+
return {
21+
mql,
22+
dispatchers,
23+
listener,
24+
};
25+
};
26+
1127
const querySubscribe = (query: string, setState: QueryStateSetter) => {
1228
let entry = queriesMap.get(query);
1329

1430
if (!entry) {
15-
const mql = matchMedia(query);
16-
const dispatchers = new Set<QueryStateSetter>();
17-
const listener = () => {
18-
dispatchers.forEach((d) => d(mql.matches));
19-
};
20-
21-
if (mql.addEventListener) mql.addEventListener('change', listener, { passive: true });
22-
else mql.addListener(listener);
23-
24-
entry = {
25-
mql,
26-
dispatchers,
27-
listener,
28-
};
31+
entry = createQueryEntry(query);
2932
queriesMap.set(query, entry);
3033
}
3134

@@ -51,15 +54,29 @@ const queryUnsubscribe = (query: string, setState: QueryStateSetter): void => {
5154
}
5255
};
5356

57+
interface UseMediaQueryOptions {
58+
initializeWithValue?: boolean;
59+
}
60+
5461
/**
5562
* Tracks the state of CSS media query.
5663
*
57-
* Return undefined initially and later receives correct value.
58-
*
5964
* @param query CSS media query to track.
65+
* @param options Hook options:
66+
* `initializeWithValue` (default: `true`) - Determine media query match state on first render. Setting
67+
* this to false will make the hook yield `undefined` on first render.
6068
*/
61-
export function useMediaQuery(query: string): boolean | undefined {
62-
const [state, setState] = useSafeState<boolean>();
69+
export function useMediaQuery(query: string, options?: UseMediaQueryOptions): boolean | undefined {
70+
const [state, setState] = useState<boolean | undefined>(() => {
71+
if (options?.initializeWithValue ?? true) {
72+
let entry = queriesMap.get(query);
73+
if (!entry) {
74+
entry = createQueryEntry(query);
75+
queriesMap.set(query, entry);
76+
}
77+
return entry.mql.matches;
78+
}
79+
});
6380

6481
useEffect(() => {
6582
querySubscribe(query, setState);

src/useScreenOrientation/__docs__/story.mdx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Canvas, Meta, Story } from '@storybook/addon-docs';
2-
import { Example } from './example.stories';
3-
import { ImportPath } from '../../__docs__/ImportPath';
1+
import { Canvas, Meta, Story } from '@storybook/addon-docs'
2+
import { Example } from './example.stories'
3+
import { ImportPath } from '../../__docs__/ImportPath'
44

55
<Meta title="Sensor/useScreenOrientation" component={Example} />
66

@@ -36,6 +36,13 @@ function useScreenOrientation(): ScreenOrientation;
3636

3737
<ImportPath />
3838

39+
#### Arguments
40+
41+
- **options** _`object`_ - Hook options:
42+
- **initializeWithValue** _`boolean`_ _(default: true)_ - Determine the screen orientation on first
43+
render. Setting this to `false` will make the hook yield `undefined` on first render. We suggest
44+
setting this to `false` during SSR.
45+
3946
#### Return
4047

41-
A string representing screen orientation
48+
A string representing the screen orientation.

src/useScreenOrientation/__tests__/dom.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ describe('useScreenOrientation', () => {
5555
expect(result.error).toBeUndefined();
5656
});
5757

58+
it('should initialize without value if initializeWithValue option is set to false', () => {
59+
const { result } = renderHook(() => useScreenOrientation({ initializeWithValue: false }));
60+
expect(result.all[0]).toBeUndefined();
61+
expect(result.all[1]).toBe('landscape');
62+
});
63+
5864
it('should return `portrait` in case media query matches and `landscape` otherwise', () => {
5965
const { result } = renderHook(() => useScreenOrientation());
6066
expect(result.current).toBe('landscape');

src/useScreenOrientation/__tests__/ssr.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ describe('useScreenOrientation', () => {
66
expect(useScreenOrientation).toBeDefined();
77
});
88

9-
it('should render', () => {
10-
const { result } = renderHook(() => useScreenOrientation());
9+
it('should render if initializeWithValue option is set to false', () => {
10+
const { result } = renderHook(() => useScreenOrientation({ initializeWithValue: false }));
1111
expect(result.error).toBeUndefined();
1212
});
1313
});

src/useScreenOrientation/useScreenOrientation.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@ import { useMediaQuery } from '../useMediaQuery/useMediaQuery';
22

33
export type ScreenOrientation = 'portrait' | 'landscape';
44

5+
interface UseScreenOrientationOptions {
6+
initializeWithValue?: boolean;
7+
}
8+
59
/**
610
* Checks if screen is in `portrait` or `landscape` orientation.
711
*
812
* As `Screen Orientation API` is still experimental and not supported by Safari, this
913
* hook uses CSS3 `orientation` media-query to check screen orientation.
1014
*/
11-
export function useScreenOrientation(): ScreenOrientation | undefined {
12-
const matches = useMediaQuery('(orientation: portrait)');
15+
export function useScreenOrientation(
16+
options?: UseScreenOrientationOptions
17+
): ScreenOrientation | undefined {
18+
const matches = useMediaQuery('(orientation: portrait)', {
19+
initializeWithValue: options?.initializeWithValue ?? true,
20+
});
1321

1422
return typeof matches === 'undefined' ? undefined : matches ? 'portrait' : 'landscape';
1523
}

0 commit comments

Comments
 (0)