Skip to content

Commit c9df89c

Browse files
committed
feat: add useMutationObserver
1 parent ed37bf3 commit c9df89c

File tree

4 files changed

+1484
-1137
lines changed

4 files changed

+1484
-1137
lines changed

package.json

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,32 @@
4646
"react": "^16.8.0"
4747
},
4848
"devDependencies": {
49-
"@4c/babel-preset": "^7.2.1",
50-
"@4c/cli": "^1.0.3",
51-
"@4c/jest-preset": "^1.4.3",
52-
"@4c/rollout": "^2.0.3",
53-
"@4c/tsconfig": "^0.3.0",
54-
"@babel/cli": "^7.7.0",
55-
"@babel/core": "^7.7.2",
49+
"@4c/babel-preset": "^7.3.3",
50+
"@4c/cli": "^2.0.1",
51+
"@4c/jest-preset": "^1.5.0",
52+
"@4c/rollout": "^2.1.2",
53+
"@4c/tsconfig": "^0.3.1",
54+
"@babel/cli": "^7.8.4",
55+
"@babel/core": "^7.8.7",
5656
"@babel/preset-typescript": "^7.7.2",
57-
"@types/enzyme": "^3.10.3",
58-
"@types/jest": "^24.0.23",
59-
"@types/react": "^16.9.12",
60-
"babel-jest": "^24.9.0",
57+
"@types/enzyme": "^3.10.5",
58+
"@types/jest": "^25.1.4",
59+
"@types/react": "^16.9.23",
60+
"babel-jest": "^25.1.0",
6161
"cherry-pick": "^0.5.0",
6262
"codecov": "^3.6.5",
6363
"enzyme": "^3.10.0",
6464
"enzyme-adapter-react-16": "^1.15.1",
6565
"eslint": "^6.7.0",
66-
"husky": "^3.1.0",
67-
"jest": "^24.9.0",
68-
"lint-staged": "^9.4.3",
66+
"husky": "^4.2.3",
67+
"jest": "^25.1.0",
68+
"lint-staged": "^10.0.8",
6969
"mq-polyfill": "^1.1.8",
7070
"prettier": "^1.19.1",
71-
"react": "^16.9.0",
72-
"react-dom": "^16.12.0",
73-
"rimraf": "^3.0.0",
74-
"typescript": "^3.7.2"
71+
"react": "^16.13.0",
72+
"react-dom": "^16.13.0",
73+
"rimraf": "^3.0.2",
74+
"typescript": "^3.8.3"
7575
},
7676
"readme": "ERROR: No README data found!",
7777
"_id": "@restart/hooks@0.3.20"

src/useMutationObserver.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import useEffect from './useIsomorphicEffect'
2+
3+
/**
4+
* Efficiently observe size changes on an element. Depends on the `ResizeObserver` api,
5+
* and polyfills are needed in older browsers.
6+
*
7+
* ```ts
8+
* const [element, attachRef] = useCallbackRef(null);
9+
*
10+
* useMutationObserver(element, { subtree: true }, (records) => {
11+
*
12+
* });
13+
*
14+
* return (
15+
* <div ref={attachRef} />
16+
* )
17+
* ```
18+
*
19+
* @param element The DOM element to observe
20+
* @param config The observer configuration
21+
* @param callback A callback fired when a mutation occurs
22+
*/
23+
function useMutationObserver(
24+
element: Element | null | undefined,
25+
config: MutationObserverInit,
26+
callback: MutationCallback,
27+
): void {
28+
const {
29+
attributeFilter,
30+
attributeOldValue,
31+
attributes,
32+
characterData,
33+
characterDataOldValue,
34+
childList,
35+
subtree,
36+
} = config
37+
38+
useEffect(() => {
39+
if (!element) return
40+
41+
const observer = new MutationObserver(callback)
42+
43+
observer.observe(element, {
44+
attributeFilter,
45+
attributeOldValue,
46+
attributes,
47+
characterData,
48+
characterDataOldValue,
49+
childList,
50+
subtree,
51+
})
52+
53+
return () => {
54+
observer.disconnect()
55+
}
56+
}, [
57+
element,
58+
callback,
59+
attributeFilter?.join(','),
60+
attributeOldValue,
61+
attributes,
62+
characterData,
63+
characterDataOldValue,
64+
childList,
65+
subtree,
66+
])
67+
}
68+
69+
export default useMutationObserver

test/useMutationObserver.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import useMutationObserver from '../src/useMutationObserver'
2+
import useCallbackRef from '../src/useCallbackRef'
3+
import React from 'react'
4+
import { mount } from 'enzyme'
5+
6+
describe('useMutationObserver', () => {
7+
it('should add a mutation observer', async () => {
8+
const teardown = jest.fn()
9+
const spy = jest.fn(() => teardown)
10+
11+
function Wrapper(props) {
12+
const [el, attachRef] = useCallbackRef()
13+
14+
useMutationObserver(el, { attributes: true }, spy)
15+
16+
return <div ref={attachRef} {...props} />
17+
}
18+
19+
const wrapper = mount(<Wrapper />)
20+
21+
expect(spy).toHaveBeenCalledTimes(0)
22+
23+
wrapper.setProps({ role: 'button' })
24+
25+
await Promise.resolve()
26+
27+
expect(spy).toHaveBeenCalledTimes(1)
28+
29+
expect(spy).toHaveBeenCalledWith(
30+
[
31+
expect.objectContaining({
32+
type: 'attributes',
33+
attributeName: 'role',
34+
}),
35+
],
36+
expect.anything(),
37+
)
38+
// coverage on the teardown
39+
wrapper.unmount()
40+
})
41+
})

0 commit comments

Comments
 (0)