Skip to content

Commit d1e1495

Browse files
authored
fix: don't clear the inView state if we get a node (#337)
* fix: don't clear the inView state if we get a node #308 * fix: use a useEffect to trigger the cleanup
1 parent 7940bfe commit d1e1495

File tree

2 files changed

+84
-8
lines changed

2 files changed

+84
-8
lines changed

src/__tests__/hooks.test.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useCallback } from 'react'
22
import { render } from '@testing-library/react'
33
import { useInView } from '../useInView'
44
import { intersectionMockInstance, mockAllIsIntersecting } from '../test-utils'
@@ -83,3 +83,75 @@ test('inView should be false when component is unmounted', () => {
8383
rerender(<HookComponent unmount />)
8484
getByText('false')
8585
})
86+
87+
const SwitchHookComponent = ({ options, toggle, unmount }) => {
88+
const [ref, inView] = useInView(options)
89+
return (
90+
<>
91+
<div
92+
data-testid="item-1"
93+
data-inview={!toggle && inView}
94+
ref={!toggle && !unmount ? ref : undefined}
95+
/>
96+
<div
97+
data-testid="item-2"
98+
data-inview={!!toggle && inView}
99+
ref={toggle && !unmount ? ref : undefined}
100+
/>
101+
</>
102+
)
103+
}
104+
105+
/**
106+
* This is a test for the case where people move the ref around (please don't)
107+
*/
108+
test('should handle ref removed', () => {
109+
const { rerender, getByTestId } = render(<SwitchHookComponent />)
110+
mockAllIsIntersecting(true)
111+
112+
const item1 = getByTestId('item-1')
113+
const item2 = getByTestId('item-2')
114+
115+
// Item1 should be inView
116+
expect(item1.getAttribute('data-inview')).toBe('true')
117+
expect(item2.getAttribute('data-inview')).toBe('false')
118+
119+
rerender(<SwitchHookComponent toggle />)
120+
mockAllIsIntersecting(true)
121+
122+
// Item2 should be inView
123+
expect(item1.getAttribute('data-inview')).toBe('false')
124+
expect(item2.getAttribute('data-inview')).toBe('true')
125+
126+
rerender(<SwitchHookComponent unmount />)
127+
128+
// Nothing should be inView
129+
expect(item1.getAttribute('data-inview')).toBe('false')
130+
expect(item2.getAttribute('data-inview')).toBe('false')
131+
132+
// Add the ref back
133+
rerender(<SwitchHookComponent />)
134+
mockAllIsIntersecting(true)
135+
expect(item1.getAttribute('data-inview')).toBe('true')
136+
expect(item2.getAttribute('data-inview')).toBe('false')
137+
})
138+
139+
const MergeRefsComponent = ({ options }) => {
140+
const [inViewRef, inView] = useInView(options)
141+
const setRef = useCallback(
142+
(node) => {
143+
inViewRef(node)
144+
},
145+
[inViewRef],
146+
)
147+
148+
return <div data-testid="inview" data-inview={inView} ref={setRef} />
149+
}
150+
151+
test('should handle ref merged', () => {
152+
const { rerender, getByTestId } = render(<MergeRefsComponent />)
153+
mockAllIsIntersecting(true)
154+
rerender(<MergeRefsComponent />)
155+
156+
expect(getByTestId('inview').getAttribute('data-inview')).toBe('true')
157+
})

src/useInView.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import * as React from 'react'
33
import { observe, unobserve } from './intersection'
44
import { InViewHookResponse, IntersectionOptions } from './index'
5+
import { useEffect } from 'react'
56

67
type State = {
78
inView: boolean
@@ -23,13 +24,8 @@ export function useInView(
2324
(node) => {
2425
if (ref.current) {
2526
unobserve(ref.current)
26-
27-
if (!options.triggerOnce) {
28-
// Reset the state, unless the hook is set to only `triggerOnce`
29-
// In that case, resetting the state would trigger another update.
30-
setState(initialState)
31-
}
3227
}
28+
3329
if (node) {
3430
observe(
3531
node,
@@ -45,11 +41,19 @@ export function useInView(
4541
)
4642
}
4743

48-
// Store a reference to the node
44+
// Store a reference to the node, so we can unobserve it later
4945
ref.current = node
5046
},
5147
[options.threshold, options.root, options.rootMargin, options.triggerOnce],
5248
)
5349

50+
useEffect(() => {
51+
if (!ref.current && state !== initialState && !options.triggerOnce) {
52+
// If we don't have a ref, then reset the state (unless the hook is set to only `triggerOnce`)
53+
// This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView
54+
setState(initialState)
55+
}
56+
})
57+
5458
return [setRef, state.inView, state.entry]
5559
}

0 commit comments

Comments
 (0)