Skip to content

Commit 11af86a

Browse files
feat(useMediaQuery): add new useMediaQuery hook
1 parent 0af7cf7 commit 11af86a

4 files changed

Lines changed: 79 additions & 0 deletions

File tree

packages/hooks/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './useConfig/index.js';
2+
export * from './useMediaQuery/index.js';
23
export * from './useMergedState/index.js';
34
export * from './useTreeCollapse/index.js';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useMediaQuery.js';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export function useMediaQuery(query) {
4+
const [matches, setMatches] = useState(() => typeof window !== 'undefined' && !!window.matchMedia(query).matches);
5+
6+
useEffect(() => {
7+
const media = window.matchMedia(query);
8+
const listener = () => setMatches(media.matches);
9+
10+
listener();
11+
12+
media.addEventListener('change', listener);
13+
return () => media.removeEventListener('change', listener);
14+
}, [query]);
15+
16+
return matches;
17+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { renderToString } from 'react-dom/server';
3+
4+
import { useMediaQuery } from './index.js';
5+
6+
describe('useMediaQuery', () => {
7+
describe('renderHook', () => {
8+
let matches = false;
9+
const listeners = new Set();
10+
11+
beforeAll(() => {
12+
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
13+
get matches() {
14+
return matches;
15+
},
16+
media: query,
17+
addEventListener: (event, cb) => listeners.add(cb),
18+
removeEventListener: (event, cb) => listeners.delete(cb),
19+
}));
20+
});
21+
22+
beforeEach(() => {
23+
matches = false;
24+
listeners.clear();
25+
});
26+
27+
afterAll(() => {
28+
window.matchMedia.mockRestore();
29+
});
30+
31+
it('should return true if the media query matches', () => {
32+
matches = true;
33+
const { result } = renderHook(() => useMediaQuery('(min-width: 600px)'));
34+
expect(result.current).toBe(true);
35+
});
36+
37+
it('should return false if the media query does not match', () => {
38+
const { result } = renderHook(() => useMediaQuery('(min-width: 1200px)'));
39+
expect(result.current).toBe(false);
40+
});
41+
42+
it('should update when the media query changes', () => {
43+
const { result } = renderHook(() => useMediaQuery('(min-width: 800px)'));
44+
expect(result.current).toBe(false);
45+
46+
act(() => {
47+
matches = true;
48+
listeners.forEach((cb) => cb());
49+
});
50+
expect(result.current).toBe(true);
51+
});
52+
});
53+
54+
describe('SSR', () => {
55+
it('should not throw during SSR and return false', () => {
56+
const TestComponent = () => String(useMediaQuery('(min-width: 600px)'));
57+
expect(renderToString(<TestComponent />)).toBe('false');
58+
});
59+
});
60+
});

0 commit comments

Comments
 (0)