Skip to content

Commit 7691780

Browse files
authored
refactor: tweak some internals for optimization (#196)
* refactor: tweak some internals for some minor optimization * test: improve testing
1 parent 76bb74a commit 7691780

File tree

6 files changed

+282
-175
lines changed

6 files changed

+282
-175
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
"@typescript-eslint/parser": "^1.4.2",
129129
"babel-core": "^7.0.0-bridge.0",
130130
"babel-eslint": "10.0.1",
131-
"babel-jest": "^24.0.0",
131+
"babel-jest": "^24.4.0",
132132
"babel-loader": "^8.0.5",
133133
"concurrently": "4.1.0",
134134
"coveralls": "^3.0.3",
@@ -140,7 +140,7 @@
140140
"eslint-plugin-react": "7.x",
141141
"husky": "^1.3.1",
142142
"intersection-observer": "^0.5.1",
143-
"jest": "^24.0.0",
143+
"jest": "^24.4.0",
144144
"jest-dom": "^3.1.1",
145145
"lint-staged": "^8.1.5",
146146
"npm-run-all": "^4.1.5",

src/InView.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,13 @@ export class InView extends React.Component<
9393
}
9494

9595
handleChange = (inView: boolean, entry: IntersectionObserverEntry) => {
96-
this.setState({ inView, entry })
96+
// Only trigger a state update if inView has changed.
97+
// This prevents an unnecessary extra state update during mount, when the element stats outside the viewport
98+
if (inView !== this.state.inView || inView) {
99+
this.setState({ inView, entry })
100+
}
97101
if (this.props.onChange) {
102+
// If the user is actively listening for onChange, always trigger it
98103
this.props.onChange(inView, entry)
99104
}
100105
}

src/__tests__/intersection.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ it('should observe with unique rootId', () => {
7878
observer: expect.any(Object),
7979
})
8080
})
81+
it('should observe with rootMargin', () => {
82+
const cb = jest.fn()
83+
const root = document.createElement('div')
84+
const instance = observe(el, cb, { root, rootMargin: '0px 0px 0px 0px' })
85+
86+
expect(instance).toMatchObject({
87+
inView: false,
88+
observerId: '1_0_0px 0px 0px 0px',
89+
observer: expect.any(Object),
90+
})
91+
})
8192

8293
it('should unobserve', () => {
8394
observe(el, jest.fn())
@@ -94,6 +105,18 @@ it('should keep observer when unobserve with multiple elements', () => {
94105
unobserve(el)
95106
})
96107

108+
it('should remove unused roots', () => {
109+
const el2 = { el: 'htmlElement2' }
110+
const el3 = { el: 'htmlElement3' }
111+
const root = document.createElement('div')
112+
observe(el, jest.fn(), { root, threshold: 0 })
113+
observe(el2, jest.fn(), { root, threshold: 0.2 })
114+
observe(el3, jest.fn(), { threshold: 0.4 })
115+
unobserve(el)
116+
unobserve(el2)
117+
unobserve(el3)
118+
})
119+
97120
it('should trigger onChange with ratio 0', () => {
98121
const cb = jest.fn()
99122
const instance = observe(el, cb)
@@ -260,3 +283,31 @@ it('should trigger clear visible when going back to 0 with array threshold', ()
260283

261284
expect(instance.inView).toBe(false)
262285
})
286+
287+
it('should use threshold if not included in IntersectionObserver instance', () => {
288+
const cb = jest.fn()
289+
IntersectionObserver.mockImplementationOnce(() => ({
290+
observe: jest.fn(),
291+
unobserve: jest.fn(),
292+
disconnect: jest.fn(),
293+
}))
294+
295+
const instance = observe(el, cb, { threshold: 0 })
296+
expect(instance).toMatchObject({
297+
thresholds: [0],
298+
})
299+
})
300+
301+
it('should use array threshold if not included in IntersectionObserver instance', () => {
302+
const cb = jest.fn()
303+
IntersectionObserver.mockImplementationOnce(() => ({
304+
observe: jest.fn(),
305+
unobserve: jest.fn(),
306+
disconnect: jest.fn(),
307+
}))
308+
309+
const instance = observe(el, cb, { threshold: [0] })
310+
expect(instance).toMatchObject({
311+
thresholds: [0],
312+
})
313+
})

src/intersection.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,19 @@ export function unobserve(element: Element | null) {
112112
let rootObserved = false
113113
/* istanbul ignore else */
114114
if (observerId) {
115-
INSTANCE_MAP.forEach((item, key) => {
116-
if (item && key !== element) {
117-
/* istanbul ignore else */
118-
if (item.observerId === observerId) itemsLeft = true
119-
/* istanbul ignore else */
120-
if (item.observer.root === root) rootObserved = true
115+
for (let [key, item] of INSTANCE_MAP) {
116+
if (key !== element) {
117+
if (item.observerId === observerId) {
118+
itemsLeft = true
119+
rootObserved = true
120+
break
121+
}
122+
if (item.observer.root === root) {
123+
rootObserved = true
124+
}
121125
}
122-
})
126+
}
123127
}
124-
125128
if (!rootObserved && root) ROOT_IDS.delete(root)
126129
if (observer && !itemsLeft) {
127130
// No more elements to observe for threshold, disconnect observer

src/useInView.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,26 @@ export function useInView(options: IntersectionOptions = {}): HookResponse {
1616

1717
React.useEffect(
1818
() => {
19-
if (ref) {
20-
observe(
21-
ref,
22-
(inView, intersection) => {
19+
if (!ref) return
20+
observe(
21+
ref,
22+
(inView, intersection) => {
23+
// Only trigger a state update if inView has changed.
24+
// This prevents an unnecessary extra state update during mount, when the element stats outside the viewport
25+
if (inView !== state.inView || inView) {
2326
setState({ inView, entry: intersection })
2427

2528
if (inView && options.triggerOnce) {
2629
// If it should only trigger once, unobserve the element after it's inView
2730
unobserve(ref)
2831
}
29-
},
30-
options,
31-
)
32-
}
32+
}
33+
},
34+
options,
35+
)
3336

3437
return () => {
35-
if (ref) unobserve(ref)
38+
unobserve(ref)
3639
}
3740
},
3841
[

0 commit comments

Comments
 (0)