Skip to content

Commit 042dd26

Browse files
authored
Fix ref issue with async elements (#35)
* Add dynamic ref story * Fix ref issue with async elements
1 parent 5ca17be commit 042dd26

File tree

5 files changed

+188
-56
lines changed

5 files changed

+188
-56
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React, { useEffect, useState } from 'react'
2+
import { storiesOf } from '@storybook/react'
3+
4+
import { useIntersectionObserver } from 'react-use-observer'
5+
6+
const IntersectionObserverAsyncRefStory = () => {
7+
const [visible, setVisible] = useState(false)
8+
const [ref, intersectionObserverEntry] = useIntersectionObserver({
9+
root: null,
10+
rootMargin: '0px',
11+
threshold: Array(1 / 0.01)
12+
.fill(0.01)
13+
.map((current, index) => current * index),
14+
})
15+
16+
const ratio = Math.floor(intersectionObserverEntry.intersectionRatio * 100)
17+
18+
useEffect(() => {
19+
// mimic async nature of data loading
20+
setTimeout(() => {
21+
setVisible(true)
22+
}, 1000)
23+
}, [])
24+
25+
return (
26+
<div
27+
style={{
28+
height: 1500,
29+
}}
30+
>
31+
<p>Scroll Me</p>
32+
{visible && (
33+
<div
34+
ref={ref}
35+
style={{
36+
position: 'relative',
37+
backgroundColor: '#f5f5f5',
38+
height: 250,
39+
width: 250,
40+
}}
41+
>
42+
<div>{`${ratio}%`}</div>
43+
<span>
44+
{intersectionObserverEntry instanceof IntersectionObserverEntry
45+
? 'has an observer entry'
46+
: 'does nott have an observer entry'}
47+
</span>
48+
<span
49+
style={{
50+
position: 'absolute',
51+
bottom: 0,
52+
right: 0,
53+
}}
54+
>
55+
{`${ratio}%`}
56+
</span>
57+
</div>
58+
)}
59+
</div>
60+
)
61+
}
62+
63+
storiesOf('useIntersectionObserver', module).add('async-ref', () => (
64+
<IntersectionObserverAsyncRefStory />
65+
))
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React, { Fragment } from 'react'
2+
import { storiesOf } from '@storybook/react'
3+
4+
import { useIntersectionObserver } from 'react-use-observer'
5+
6+
const Box = React.forwardRef(({ ratio, children, ...props }, ref) => (
7+
<div
8+
ref={ref}
9+
style={{
10+
position: 'relative',
11+
backgroundColor: '#f5f5f5',
12+
height: 250,
13+
width: 250,
14+
display: 'flex',
15+
justifyContent: 'center',
16+
alignItems: 'center',
17+
}}
18+
{...props}
19+
>
20+
<span
21+
style={{
22+
position: 'absolute',
23+
top: 0,
24+
left: 0,
25+
}}
26+
>
27+
{`${ratio}%`}
28+
</span>
29+
{children}
30+
<span
31+
style={{
32+
position: 'absolute',
33+
bottom: 0,
34+
right: 0,
35+
}}
36+
>
37+
{`${ratio}%`}
38+
</span>
39+
</div>
40+
))
41+
42+
const dummyArray = new Array(5).fill('')
43+
44+
const DynamicRefIntersectionObserverStory = () => {
45+
const [firstRef, firstEntry] = useIntersectionObserver({
46+
threshold: [0.5],
47+
})
48+
49+
const ratio = Math.floor((firstEntry ? firstEntry.intersectionRatio : 1) * 100)
50+
51+
return (
52+
<Fragment>
53+
<p>Scroll Me</p>
54+
<div
55+
style={{
56+
height: 1500,
57+
display: 'flex',
58+
justifyContent: 'space-between',
59+
}}
60+
>
61+
{dummyArray.map((_, index) => (
62+
<Box
63+
key={`key${index}`}
64+
ref={index === 0 ? firstRef : undefined}
65+
ratio={index === 0 ? ratio : 100}
66+
/>
67+
))}
68+
</div>
69+
</Fragment>
70+
)
71+
}
72+
73+
storiesOf('useIntersectionObserver', module).add('Dynamic Ref', () => (
74+
<DynamicRefIntersectionObserverStory />
75+
))

examples/yarn.lock

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -878,13 +878,6 @@
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-
888881
"@babel/template@^7.1.0", "@babel/template@^7.2.2", "@babel/template@^7.4.4":
889882
version "7.4.4"
890883
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
@@ -1451,30 +1444,11 @@
14511444
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.2.tgz#3452a24edf9fea138b48fad4a0a028a683da1e40"
14521445
integrity sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==
14531446

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-
14591447
"@types/q@^1.5.1":
14601448
version "1.5.2"
14611449
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
14621450
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
14631451

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-
14781452
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
14791453
version "2.0.3"
14801454
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
@@ -3130,11 +3104,6 @@ csstype@^2.5.7:
31303104
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.4.tgz#d585a6062096e324e7187f80e04f92bd0f00e37f"
31313105
integrity sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==
31323106

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-
31383107
cyclist@~0.2.2:
31393108
version "0.2.2"
31403109
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
@@ -3225,11 +3194,6 @@ depd@~1.1.2:
32253194
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
32263195
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
32273196

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-
32333197
des.js@^1.0.0:
32343198
version "1.0.0"
32353199
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
@@ -3700,6 +3664,11 @@ fast-deep-equal@^2.0.1:
37003664
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
37013665
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
37023666

3667+
fast-deep-equal@^3.1.3:
3668+
version "3.1.3"
3669+
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
3670+
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
3671+
37033672
fast-glob@^2.0.2:
37043673
version "2.2.7"
37053674
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
@@ -6474,11 +6443,6 @@ regenerator-runtime@^0.13.2:
64746443
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
64756444
integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
64766445

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-
64826446
regenerator-transform@^0.13.4:
64836447
version "0.13.4"
64846448
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb"
@@ -7566,15 +7530,6 @@ url@^0.11.0:
75667530
punycode "1.3.2"
75677531
querystring "0.2.0"
75687532

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-
75787533
use@^3.1.0:
75797534
version "3.1.1"
75807535
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"

src/useObserver.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useRef, useState } from 'react'
1+
import { useState, useCallback } from 'react'
22
import useDeepCompareEffect from './helpers/useDeepCompareEffect'
33

44
import createObserverPublisher from './helpers/createObserverPublisher'
@@ -15,8 +15,14 @@ export default function useObserver(Observer, { observerOptions, subscribeOption
1515
}
1616
}
1717

18-
const ref = useRef(null)
1918
const [entry, setEntry] = useState({})
19+
const [element, setElement] = useState()
20+
21+
const ref = useCallback((node) => {
22+
if (node !== null) {
23+
setElement(node)
24+
}
25+
}, [])
2026

2127
useDeepCompareEffect(() => {
2228
if (!publishers) {
@@ -37,8 +43,6 @@ export default function useObserver(Observer, { observerOptions, subscribeOption
3743
options.set(optionId, publisher)
3844
}
3945

40-
const element = ref.current
41-
4246
if (!element) {
4347
return undefined
4448
}
@@ -47,7 +51,7 @@ export default function useObserver(Observer, { observerOptions, subscribeOption
4751
subscribe({ element, options: subscribeOptions }, newEntry => setEntry(newEntry))
4852

4953
return () => unsubscribe(element)
50-
}, [Observer, observerOptions, subscribeOptions])
54+
}, [element, Observer, observerOptions, subscribeOptions])
5155

5256
return [ref, entry]
5357
}

src/useObserver.test.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useEffect, useState } from 'react'
22
import { renderHook } from 'react-hooks-testing-library'
33
import { render, act } from 'react-testing-library'
44

@@ -160,3 +160,36 @@ it('should update entry for element on change', () => {
160160
expect(element1.textContent).toBe('test1')
161161
expect(element2.textContent).toBe('test2')
162162
})
163+
164+
it('should work with async elements', () => {
165+
jest.useFakeTimers()
166+
const TestComp = () => {
167+
const [ref] = useObserver(MockObserver)
168+
const [visible, setVisible] = useState(false)
169+
useEffect(() => {
170+
// mimic async nature of data loading
171+
setTimeout(() => {
172+
setVisible(true)
173+
}, 1000)
174+
}, [])
175+
176+
return (
177+
<div>
178+
{visible && <div ref={ref} />}
179+
</div>
180+
)
181+
}
182+
183+
const { unmount, container } = render(<TestComp />)
184+
185+
act(() => {
186+
jest.runAllTimers()
187+
})
188+
189+
const element = container.firstChild.firstChild
190+
191+
expect(mockObserve).toBeCalledWith(element, undefined)
192+
unmount()
193+
expect(mockUnobserve).toBeCalledWith(element)
194+
jest.useRealTimers()
195+
})

0 commit comments

Comments
 (0)