Skip to content

Commit cfa52dc

Browse files
committed
better implementation of useMutationObserver
1 parent b85ad85 commit cfa52dc

File tree

6 files changed

+118
-42
lines changed

6 files changed

+118
-42
lines changed

.babelrc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
"modules": false
1010
}
1111
]
12+
],
13+
"plugins": [
14+
[
15+
"babel-plugin-transform-rename-import",
16+
{
17+
"original": "lodash",
18+
"replacement": "lodash-es"
19+
}
20+
]
1221
]
1322
},
1423
"test": {

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@
5656
"@babel/preset-typescript": "^7.7.2",
5757
"@types/enzyme": "^3.10.5",
5858
"@types/jest": "^25.1.4",
59+
"@types/lodash": "^4.14.149",
5960
"@types/react": "^16.9.23",
6061
"babel-jest": "^25.1.0",
62+
"babel-plugin-transform-rename-import": "^2.3.0",
6163
"cherry-pick": "^0.5.0",
6264
"codecov": "^3.6.5",
6365
"enzyme": "^3.10.0",
@@ -73,6 +75,8 @@
7375
"rimraf": "^3.0.2",
7476
"typescript": "^3.8.3"
7577
},
76-
"readme": "ERROR: No README data found!",
77-
"_id": "@restart/[email protected]"
78+
"dependencies": {
79+
"lodash": "^4.17.15",
80+
"lodash-es": "^4.17.15"
81+
}
7882
}

src/useCustomEffect.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type CustomEffectOptions<TDeps> = {
1212
/**
1313
* a useEffect() hook with customized depedency comparision
1414
*
15-
* @param effect THe effect callback
15+
* @param effect The effect callback
1616
* @param dependencies A list of dependencies
1717
* @param isEqual A function comparing the next and previous dependencyLists
1818
*/
@@ -24,7 +24,7 @@ function useCustomEffect<TDeps extends DependencyList = DependencyList>(
2424
/**
2525
* a useEffect() hook with customized depedency comparision
2626
*
27-
* @param effect THe effect callback
27+
* @param effect The effect callback
2828
* @param dependencies A list of dependencies
2929
* @param options
3030
* @param options.isEqual A function comparing the next and previous dependencyLists

src/useMutationObserver.ts

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
import useEffect from './useIsomorphicEffect'
1+
import useCustomEffect, { IsEqual } from './useCustomEffect'
2+
import isEqual from 'lodash/isEqual'
3+
import useImmediateUpdateEffect from './useImmediateUpdateEffect'
4+
import useMountEffect from './useMountEffect'
5+
import useEventCallback from './useEventCallback'
6+
import { useRef } from 'react'
7+
8+
type Deps = [Element | null | undefined, MutationObserverInit]
9+
10+
const isDepsEqual: IsEqual<Deps> = (prev, next) =>
11+
prev[0] === next[0] && isEqual(prev[1], next[1])
212

313
/**
4-
* Efficiently observe size changes on an element. Depends on the `ResizeObserver` api,
5-
* and polyfills are needed in older browsers.
14+
* Observe mutations on a DOM node or tree of DOM nodes.
15+
* Depends on the `MutationObserver` api.
616
*
717
* ```ts
818
* const [element, attachRef] = useCallbackRef(null);
@@ -25,45 +35,34 @@ function useMutationObserver(
2535
config: MutationObserverInit,
2636
callback: MutationCallback,
2737
): void {
28-
const {
29-
attributeFilter,
30-
attributeOldValue,
31-
attributes,
32-
characterData,
33-
characterDataOldValue,
34-
childList,
35-
subtree,
36-
} = config
38+
const observerRef = useRef<MutationObserver | null>()
39+
const fn = useEventCallback(callback)
3740

38-
useEffect(() => {
41+
useMountEffect(() => {
3942
if (!element) return
4043

41-
const observer = new MutationObserver(callback)
44+
observerRef.current = new MutationObserver(fn)
45+
})
46+
47+
useCustomEffect(
48+
() => {
49+
if (!element) return
4250

43-
observer.observe(element, {
44-
attributeFilter,
45-
attributeOldValue,
46-
attributes,
47-
characterData,
48-
characterDataOldValue,
49-
childList,
50-
subtree,
51-
})
51+
const observer = observerRef.current || new MutationObserver(fn)
52+
observer.observe(element, config)
5253

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-
])
54+
return () => {
55+
observer.disconnect()
56+
}
57+
},
58+
[element, config],
59+
{
60+
isEqual: isDepsEqual,
61+
// Intentionally done in render, otherwise observer will miss any
62+
// changes made to the DOM during this update
63+
effectHook: useImmediateUpdateEffect,
64+
},
65+
)
6766
}
6867

6968
export default useMutationObserver

test/useMutationObserver.test.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('useMutationObserver', () => {
99
const spy = jest.fn(() => teardown)
1010

1111
function Wrapper(props) {
12-
const [el, attachRef] = useCallbackRef()
12+
const [el, attachRef] = useCallbackRef<HTMLElement>()
1313

1414
useMutationObserver(el, { attributes: true }, spy)
1515

@@ -38,4 +38,53 @@ describe('useMutationObserver', () => {
3838
// coverage on the teardown
3939
wrapper.unmount()
4040
})
41+
42+
let disconnentSpy: jest.SpyInstance<void, []>
43+
afterEach(() => {
44+
disconnentSpy?.mockRestore()
45+
})
46+
47+
it.only('should update config', async () => {
48+
const teardown = jest.fn()
49+
const spy = jest.fn(() => teardown)
50+
51+
disconnentSpy = jest.spyOn(MutationObserver.prototype, 'disconnect')
52+
53+
function Wrapper({ attributeFilter, ...props }) {
54+
const [el, attachRef] = useCallbackRef<HTMLElement>()
55+
56+
useMutationObserver(el, { attributes: true, attributeFilter }, spy)
57+
58+
return <div ref={attachRef} {...props} />
59+
}
60+
61+
const wrapper = mount(<Wrapper attributeFilter={['data-name']} />)
62+
63+
wrapper.setProps({ role: 'presentation' })
64+
65+
await Promise.resolve()
66+
67+
// console.log(spy.mock.calls[0][0][0].attributes)
68+
expect(spy).toHaveBeenCalledTimes(0)
69+
70+
wrapper.setProps({ attributeFilter: undefined, role: 'button' })
71+
72+
await Promise.resolve()
73+
74+
expect(spy).toHaveBeenCalledTimes(1)
75+
76+
expect(spy).toHaveBeenCalledWith(
77+
[
78+
expect.objectContaining({
79+
type: 'attributes',
80+
attributeName: 'role',
81+
}),
82+
],
83+
expect.anything(),
84+
)
85+
86+
wrapper.unmount()
87+
88+
expect(disconnentSpy).toBeCalledTimes(2)
89+
})
4190
})

yarn.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,11 @@
15531553
jest-diff "^25.1.0"
15541554
pretty-format "^25.1.0"
15551555

1556+
"@types/lodash@^4.14.149":
1557+
version "4.14.149"
1558+
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"
1559+
integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==
1560+
15561561
"@types/minimatch@*":
15571562
version "3.0.3"
15581563
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -2055,6 +2060,11 @@ babel-plugin-react-intl@^5.1.16:
20552060
intl-messageformat-parser "^3.6.3"
20562061
schema-utils "^2.2.0"
20572062

2063+
babel-plugin-transform-rename-import@^2.3.0:
2064+
version "2.3.0"
2065+
resolved "https://registry.yarnpkg.com/babel-plugin-transform-rename-import/-/babel-plugin-transform-rename-import-2.3.0.tgz#5d9d645f937b0ca5c26a24b2510a06277b6ffd9b"
2066+
integrity sha512-dPgJoT57XC0PqSnLgl2FwNvxFrWlspatX2dkk7yjKQj5HHGw071vAcOf+hqW8ClqcBDMvEbm6mevn5yHAD8mlQ==
2067+
20582068
babel-preset-jest@^25.1.0:
20592069
version "25.1.0"
20602070
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-25.1.0.tgz#d0aebfebb2177a21cde710996fce8486d34f1d33"
@@ -6053,6 +6063,11 @@ locate-path@^5.0.0:
60536063
dependencies:
60546064
p-locate "^4.1.0"
60556065

6066+
lodash-es@^4.17.15:
6067+
version "4.17.15"
6068+
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
6069+
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
6070+
60566071
lodash._reinterpolate@^3.0.0:
60576072
version "3.0.0"
60586073
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"

0 commit comments

Comments
 (0)