Skip to content

Commit e90d576

Browse files
authored
feat: Clone useDeepCompareEffect internally to support IE9+ (#24)
* feat: Clone useDeepCompareEffect internally to support IE9+ * Add useDeepCompareTest
1 parent 0582a5b commit e90d576

File tree

9 files changed

+255
-32
lines changed

9 files changed

+255
-32
lines changed

examples/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"prop-types": "^15.6.2",
1313
"react": "^16.8.6",
1414
"react-dom": "^16.8.6",
15-
"react-use-observer": "file:../dist"
15+
"react-use-observer": "link:../dist"
1616
},
1717
"scripts": {
1818
"start": "start-storybook"

examples/yarn.lock

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,13 @@
878878
dependencies:
879879
regenerator-runtime "^0.13.2"
880880

881+
"@babel/runtime@^7.12.5":
882+
version "7.13.10"
883+
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
884+
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
885+
dependencies:
886+
regenerator-runtime "^0.13.4"
887+
881888
"@babel/template@^7.1.0", "@babel/template@^7.2.2", "@babel/template@^7.4.4":
882889
version "7.4.4"
883890
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
@@ -1444,11 +1451,30 @@
14441451
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.2.tgz#3452a24edf9fea138b48fad4a0a028a683da1e40"
14451452
integrity sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==
14461453

1454+
"@types/prop-types@*":
1455+
version "15.7.3"
1456+
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
1457+
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
1458+
14471459
"@types/q@^1.5.1":
14481460
version "1.5.2"
14491461
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
14501462
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
14511463

1464+
"@types/react@^17.0.0":
1465+
version "17.0.3"
1466+
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79"
1467+
integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==
1468+
dependencies:
1469+
"@types/prop-types" "*"
1470+
"@types/scheduler" "*"
1471+
csstype "^3.0.2"
1472+
1473+
"@types/scheduler@*":
1474+
version "0.16.1"
1475+
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
1476+
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
1477+
14521478
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
14531479
version "2.0.3"
14541480
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
@@ -3104,6 +3130,11 @@ csstype@^2.5.7:
31043130
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.4.tgz#d585a6062096e324e7187f80e04f92bd0f00e37f"
31053131
integrity sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==
31063132

3133+
csstype@^3.0.2:
3134+
version "3.0.7"
3135+
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b"
3136+
integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==
3137+
31073138
cyclist@~0.2.2:
31083139
version "0.2.2"
31093140
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
@@ -3194,6 +3225,11 @@ depd@~1.1.2:
31943225
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
31953226
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
31963227

3228+
dequal@^2.0.2:
3229+
version "2.0.2"
3230+
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
3231+
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
3232+
31973233
des.js@^1.0.0:
31983234
version "1.0.0"
31993235
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
@@ -6308,6 +6344,7 @@ react-textarea-autosize@^7.0.4:
63086344

63096345
"react-use-observer@link:../dist":
63106346
version "0.0.0"
6347+
uid ""
63116348

63126349
react@^16.8.1, react@^16.8.6:
63136350
version "16.8.6"
@@ -6437,6 +6474,11 @@ regenerator-runtime@^0.13.2:
64376474
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
64386475
integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
64396476

6477+
regenerator-runtime@^0.13.4:
6478+
version "0.13.7"
6479+
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
6480+
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
6481+
64406482
regenerator-transform@^0.13.4:
64416483
version "0.13.4"
64426484
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb"
@@ -7524,6 +7566,15 @@ url@^0.11.0:
75247566
punycode "1.3.2"
75257567
querystring "0.2.0"
75267568

7569+
use-deep-compare-effect@^1.2.0:
7570+
version "1.6.1"
7571+
resolved "https://registry.yarnpkg.com/use-deep-compare-effect/-/use-deep-compare-effect-1.6.1.tgz#061a0ac5400aa0461e33dddfaa2a98bca873182a"
7572+
integrity sha512-VB3b+7tFI81dHm8buGyrpxi8yBhzYZdyMX9iBJra7SMFMZ4ci4FJ1vFc1nvChiB1iLv4GfjqaYfvbNEpTT1rFQ==
7573+
dependencies:
7574+
"@babel/runtime" "^7.12.5"
7575+
"@types/react" "^17.0.0"
7576+
dequal "^2.0.2"
7577+
75277578
use@^3.1.0:
75287579
version "3.1.1"
75297580
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@
8181
"standard-version": "^6.0.1"
8282
},
8383
"dependencies": {
84-
"global": "^4.3.2",
85-
"use-deep-compare-effect": "^1.2.0"
84+
"fast-deep-equal": "^3.1.3",
85+
"global": "^4.3.2"
8686
},
8787
"config": {
8888
"commitizen": {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copy of https://github.com/kentcdodds/use-deep-compare-effect
3+
uses fast-deep-equal instead to support IE9+
4+
*/
5+
import React from 'react'
6+
import deepEqual from 'fast-deep-equal'
7+
8+
function isPrimitive(val) {
9+
return val == null || /^[sbn]/.test(typeof val)
10+
}
11+
12+
function checkDeps(deps) {
13+
if (!deps || !deps.length) {
14+
throw new Error('useDeepCompareEffect should not be used with no dependencies. Use React.useEffect instead.')
15+
}
16+
if (deps.every(isPrimitive)) {
17+
throw new Error('useDeepCompareEffect should not be used with dependencies that are all primitive values. Use React.useEffect instead.')
18+
}
19+
}
20+
21+
function useDeepCompareMemoize(value) {
22+
const ref = React.useRef()
23+
const signalRef = React.useRef(0)
24+
25+
if (!deepEqual(value, ref.current)) {
26+
ref.current = value
27+
signalRef.current += 1
28+
}
29+
return [signalRef.current]
30+
}
31+
function useDeepCompareEffect(callback, dependencies) {
32+
if (process.env.NODE_ENV !== 'production') {
33+
checkDeps(dependencies)
34+
}
35+
// eslint-disable-next-line react-hooks/exhaustive-deps
36+
return React.useEffect(callback, useDeepCompareMemoize(dependencies))
37+
}
38+
export function useDeepCompareEffectNoCheck(callback, dependencies) {
39+
// eslint-disable-next-line react-hooks/exhaustive-deps
40+
return React.useEffect(callback, useDeepCompareMemoize(dependencies))
41+
}
42+
export default useDeepCompareEffect
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
Copy of https://github.com/kentcdodds/use-deep-compare-effect
3+
uses fast-deep-equal instead to support IE9+
4+
*/
5+
6+
import { useState } from 'react'
7+
import { renderHook } from 'react-hooks-testing-library'
8+
import useDeepCompareEffect, { useDeepCompareEffectNoCheck } from './useDeepCompareEffect'
9+
10+
test('useDeepCompareEffect throws an error if using it with an empty array', () => {
11+
const { result } = renderHook(() => useDeepCompareEffect(() => {}, []))
12+
expect(result.error).toMatchInlineSnapshot('[Error: useDeepCompareEffect should not be used with no dependencies. Use React.useEffect instead.]')
13+
})
14+
15+
test('useDeepCompareEffect throws an error if using it with an array of only primitive values', () => {
16+
const { result } = renderHook(() => useDeepCompareEffect(() => {}, [true, 1, 'string']))
17+
expect(result.error).toMatchInlineSnapshot('[Error: useDeepCompareEffect should not be used with dependencies that are all primitive values. Use React.useEffect instead.]')
18+
})
19+
20+
test("useDeepCompareEffectNoCheck don't throw an error if using it with an array of only primitive values", () => {
21+
const errorMock = jest.spyOn(console, 'error').mockImplementation(() => {})
22+
expect(() => renderHook(() => useDeepCompareEffectNoCheck(() => {}, [true, 1, 'string']))).not.toThrow()
23+
expect(console.error).toHaveBeenCalledTimes(0)
24+
errorMock.mockRestore()
25+
})
26+
27+
test('in production mode there are no errors thrown', () => {
28+
const env = process.env.NODE_ENV
29+
process.env.NODE_ENV = 'production'
30+
renderHook(() => useDeepCompareEffect(() => {}, [true, 1, 'string']))
31+
renderHook(() => useDeepCompareEffect(() => {}, []))
32+
process.env.NODE_ENV = env
33+
})
34+
35+
test('useDeepCompareEffect handles changing values as expected', () => {
36+
const callback = jest.fn()
37+
let deps = [1, { a: 'b' }, true]
38+
const { rerender } = renderHook(() => useDeepCompareEffect(callback, deps))
39+
40+
expect(callback).toHaveBeenCalledTimes(1)
41+
callback.mockClear()
42+
43+
// no change
44+
rerender()
45+
expect(callback).toHaveBeenCalledTimes(0)
46+
callback.mockClear()
47+
48+
// no-change (new object with same properties)
49+
deps = [1, { a: 'b' }, true]
50+
rerender()
51+
expect(callback).toHaveBeenCalledTimes(0)
52+
callback.mockClear()
53+
54+
// change (new primitive value)
55+
deps = [2, { a: 'b' }, true]
56+
rerender()
57+
expect(callback).toHaveBeenCalledTimes(1)
58+
callback.mockClear()
59+
60+
// no-change
61+
rerender()
62+
expect(callback).toHaveBeenCalledTimes(0)
63+
callback.mockClear()
64+
65+
// change (new primitive value)
66+
deps = [1, { a: 'b' }, false]
67+
rerender()
68+
expect(callback).toHaveBeenCalledTimes(1)
69+
callback.mockClear()
70+
71+
// change (new properties on object)
72+
deps = [1, { a: 'c' }, false]
73+
rerender()
74+
expect(callback).toHaveBeenCalledTimes(1)
75+
callback.mockClear()
76+
})
77+
78+
// this may be useful in the future, but we don't support it today so I thought
79+
// it'd be good to include as a test as it would be a breaking change if we
80+
// did add support. I'm inclined to not support this. Manipulation is not good.
81+
test('useDeepCompareEffect does NOT work with manipulation', () => {
82+
const callback = jest.fn()
83+
const deps = [{ a: 'b' }]
84+
const { rerender } = renderHook(() => useDeepCompareEffect(callback, deps))
85+
expect(callback).toHaveBeenCalledTimes(1)
86+
callback.mockClear()
87+
88+
deps[0].a = 'c'
89+
rerender()
90+
expect(callback).toHaveBeenCalledTimes(0)
91+
})
92+
93+
test('useDeepCompareEffect works with deep object similarities/differences', () => {
94+
const callback = jest.fn()
95+
let deps = [{ a: { b: { c: 'd' } } }]
96+
const { rerender } = renderHook(() => useDeepCompareEffect(callback, deps))
97+
expect(callback).toHaveBeenCalledTimes(1)
98+
callback.mockClear()
99+
100+
// change primitive value
101+
deps = [{ a: { b: { c: 'e' } } }]
102+
rerender()
103+
expect(callback).toHaveBeenCalledTimes(1)
104+
callback.mockClear()
105+
106+
// no-change
107+
deps = [{ a: { b: { c: 'e' } } }]
108+
rerender()
109+
expect(callback).toHaveBeenCalledTimes(0)
110+
callback.mockClear()
111+
112+
// add property
113+
deps = [{ a: { b: { c: 'e' }, f: 'g' } }]
114+
rerender()
115+
expect(callback).toHaveBeenCalledTimes(1)
116+
callback.mockClear()
117+
})
118+
119+
test('useDeepCompareEffect works with getDerivedStateFromProps', () => {
120+
const callback = jest.fn()
121+
const { rerender } = renderHook(
122+
({ a }) => {
123+
const [lastA, setLastA] = useState(a)
124+
const [c, setC] = useState(5)
125+
if (lastA !== a) {
126+
setLastA(a)
127+
setC(1)
128+
}
129+
useDeepCompareEffect(callback, [{ a, c }])
130+
},
131+
{ initialProps: { a: 1 } },
132+
)
133+
expect(callback).toHaveBeenCalledTimes(1)
134+
callback.mockClear()
135+
136+
// change a, and reset c
137+
rerender({ a: 2 })
138+
expect(callback).toHaveBeenCalledTimes(1)
139+
callback.mockClear()
140+
})
141+
142+
/* eslint no-console:0 */

src/useIntersectionObserver.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jest.mock('global/window', () => ({
1212
jest.mock('./useObserver')
1313

1414
it('should call useObserver with IntersectionObserver and observerOptions', () => {
15-
renderHook(() => useIntersectionObserver({ test: 'test' }));
15+
renderHook(() => useIntersectionObserver({ test: 'test' }))
1616

1717
expect(useObserver).toBeCalledWith(window.IntersectionObserver, {
1818
observerOptions: {

src/useObserver.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useRef, useState } from 'react'
2-
import useDeepCompareEffect from 'use-deep-compare-effect'
2+
import useDeepCompareEffect from './helpers/useDeepCompareEffect'
33

44
import createObserverPublisher from './helpers/createObserverPublisher'
55

@@ -8,7 +8,8 @@ let publishers
88
export default function useObserver(Observer, { observerOptions, subscribeOptions } = {}) {
99
if (process.env.NODE_ENV !== 'production') {
1010
if (!Observer) {
11-
throw new Error(
11+
// eslint-disable-next-line no-console
12+
console.error(
1213
'useObserver requires a valid WebAPI Observer as a first parameter',
1314
)
1415
}

src/useObserver.test.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,28 @@ beforeEach(() => {
2626
it('should throw an error if no Observer given', () => {
2727
jest.spyOn(console, 'error').mockImplementation(() => { })
2828

29-
const { result } = renderHook(() => useObserver())
30-
31-
expect(result.error.message)
32-
.toBe('useObserver requires a valid WebAPI Observer as a first parameter')
29+
renderHook(() => useObserver())
3330

3431
// eslint-disable-next-line no-console
35-
expect(console.error)
36-
.toHaveBeenCalledTimes(1)
32+
expect(console.error).toBeCalledWith('useObserver requires a valid WebAPI Observer as a first parameter')
3733

3834
// eslint-disable-next-line no-console
3935
console.error.mockRestore()
4036
})
4137

4238
it('should not throw an error on production mode', () => {
39+
jest.spyOn(console, 'error').mockImplementation(() => {})
4340
const env = process.env.NODE_ENV
4441
process.env.NODE_ENV = 'production'
4542

4643
const { result } = renderHook(() => useObserver())
4744
expect(result.error).toBeUndefined()
4845

46+
// eslint-disable-next-line no-console
47+
expect(console.error).not.toBeCalled()
48+
// eslint-disable-next-line no-console
49+
console.error.mockRestore()
50+
4951
process.env.NODE_ENV = env
5052
})
5153

0 commit comments

Comments
 (0)