Skip to content

Commit 34c56f6

Browse files
author
Tom Lienard
authored
feat: add use-media fork to support react 18 (#766)
* feat: add `use-media` fork to support react 18 * feat: add simple tests * refactor: remove MediaQueryObject to only accept strings query * fix: resolve review * fix: tsc error
1 parent 498d337 commit 34c56f6

File tree

8 files changed

+221
-0
lines changed

8 files changed

+221
-0
lines changed

packages/use-media/.eslintrc.cjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const { join } = require('path')
2+
3+
module.exports = {
4+
rules: {
5+
'import/no-extraneous-dependencies': [
6+
'error',
7+
{ packageDir: [__dirname, join(__dirname, '../../')] },
8+
],
9+
},
10+
}

packages/use-media/.npmignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
**/__tests__/**
2+
examples/
3+
src
4+
.eslintrc.cjs
5+
!.npmignore

packages/use-media/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# `@scaleway/use-media`
2+
3+
## A small hook to track CSS media query state
4+
5+
This library has been forked from [use-media](https://github.com/streamich/use-media), many thanks to the original author, [Vadim Dalecky](https://github.com/streamich).
6+
7+
## Install
8+
9+
```bash
10+
$ pnpm add @scaleway/use-media
11+
```
12+
13+
## Usage
14+
15+
### With `useEffect`
16+
17+
```tsx
18+
import { useMedia } from '@scaleway/use-media'
19+
20+
const App = () => {
21+
// Accepts an object of features to test
22+
const isWide = useMedia({ minWidth: '1000px' });
23+
// Or a regular media query string
24+
const reduceMotion = useMedia('(prefers-reduced-motion: reduce)');
25+
26+
return (
27+
<div>
28+
Screen is wide: {isWide ? '😃' : '😢'}
29+
</div>
30+
);
31+
}
32+
```
33+
34+
### With `useLayoutEffect`
35+
36+
```tsx
37+
import { useMediaLayout } from '@scaleway/use-media'
38+
39+
const App = () => {
40+
// Accepts an object of features to test
41+
const isWide = useMediaLayout({ minWidth: '1000px' });
42+
// Or a regular media query string
43+
const reduceMotion = useMediaLayout('(prefers-reduced-motion: reduce)');
44+
45+
return (
46+
<div>
47+
Screen is wide: {isWide ? '😃' : '😢'}
48+
</div>
49+
);
50+
}
51+
```

packages/use-media/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@scaleway/use-media",
3+
"version": "1.0.0",
4+
"description": "A small hook to track CSS media query state",
5+
"keywords": [
6+
"react",
7+
"reactjs",
8+
"hooks",
9+
"media",
10+
"media queries"
11+
],
12+
"type": "module",
13+
"main": "dist/index.js",
14+
"module": "dist/index.js",
15+
"types": "dist/index.d.ts",
16+
"browser": {
17+
"dist/index.js": "./dist/index.browser.js"
18+
},
19+
"publishConfig": {
20+
"access": "public"
21+
},
22+
"repository": {
23+
"type": "git",
24+
"url": "https://github.com/scaleway/scaleway-lib",
25+
"directory": "packages/use-media"
26+
},
27+
"license": "MIT",
28+
"peerDependencies": {
29+
"react": "17.x || 18.x"
30+
}
31+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { renderHook } from '@testing-library/react-hooks'
2+
import { useMedia } from '..'
3+
4+
describe('useMedia hook', () => {
5+
it('should return the result of a query with a string', () => {
6+
const { result } = renderHook(() =>
7+
useMedia('screen and (min-width: 1000px)'),
8+
)
9+
10+
expect(result.current).toBe(false)
11+
})
12+
13+
it('should call onChange', () => {
14+
const mockAddEventListener = (_event: string, callback: () => void) =>
15+
callback()
16+
17+
Object.defineProperty(window, 'matchMedia', {
18+
value: jest.fn().mockImplementation((query: string) => ({
19+
addEventListener: mockAddEventListener,
20+
addListener: jest.fn(),
21+
dispatchEvent: jest.fn(),
22+
matches: false,
23+
media: query,
24+
onchange: null,
25+
removeEventListener: jest.fn(),
26+
removeListener: jest.fn(),
27+
})),
28+
writable: true,
29+
})
30+
31+
const { result } = renderHook(() =>
32+
useMedia('screen and (min-width: 1000px)'),
33+
)
34+
35+
expect(result.current).toBe(false)
36+
})
37+
38+
it('should not call onChange when unmounted', () => {
39+
let callback: () => void
40+
41+
const mockAddEventListener = (_event: string, callbackFn: () => void) => {
42+
callback = callbackFn
43+
}
44+
45+
Object.defineProperty(window, 'matchMedia', {
46+
value: jest.fn().mockImplementation((query: string) => ({
47+
addEventListener: mockAddEventListener,
48+
addListener: jest.fn(),
49+
dispatchEvent: jest.fn(),
50+
matches: false,
51+
media: query,
52+
onchange: null,
53+
removeEventListener: jest.fn(),
54+
removeListener: jest.fn(),
55+
})),
56+
writable: true,
57+
})
58+
59+
const { result, unmount } = renderHook(() =>
60+
useMedia('screen and (min-width: 1000px)'),
61+
)
62+
63+
unmount()
64+
// @ts-expect-error variable is assigned inside mockAddEventListener
65+
callback()
66+
67+
expect(result.current).toBe(false)
68+
})
69+
})

packages/use-media/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { useMedia, useMediaLayout } from './useMedia'

packages/use-media/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { DependencyList, EffectCallback } from 'react'
2+
3+
export type Effect = (effect: EffectCallback, deps?: DependencyList) => void

packages/use-media/src/useMedia.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useEffect, useLayoutEffect, useState } from 'react'
2+
import { Effect } from './types'
3+
4+
function noop() {}
5+
6+
export const mockMediaQueryList: MediaQueryList = {
7+
addEventListener: noop,
8+
addListener: noop,
9+
dispatchEvent: /* istanbul ignore next */ () => true,
10+
matches: false,
11+
media: '',
12+
onchange: noop,
13+
removeEventListener: noop,
14+
removeListener: noop,
15+
}
16+
17+
const createUseMedia =
18+
(effect: Effect) =>
19+
(query: string, defaultState = false) => {
20+
const [state, setState] = useState(defaultState)
21+
22+
effect(() => {
23+
let mounted = true
24+
const mediaQueryList: MediaQueryList =
25+
typeof window === 'undefined' ||
26+
typeof window.matchMedia === 'undefined'
27+
? mockMediaQueryList
28+
: window.matchMedia(query)
29+
30+
const onChange = () => {
31+
if (!mounted) {
32+
return
33+
}
34+
35+
setState(Boolean(mediaQueryList.matches))
36+
}
37+
38+
mediaQueryList.addEventListener('change', onChange)
39+
setState(mediaQueryList.matches)
40+
41+
return () => {
42+
mounted = false
43+
mediaQueryList.removeEventListener('change', onChange)
44+
}
45+
}, [query])
46+
47+
return state
48+
}
49+
50+
export const useMedia = createUseMedia(useEffect)
51+
export const useMediaLayout = createUseMedia(useLayoutEffect)

0 commit comments

Comments
 (0)