Skip to content

Commit 8f4cc9e

Browse files
dijsmarkbrocato
andauthored
Handling no IntersectionObserver (#90)
* Handling no IntersectionObserver * Refactored the use of intersection observer * Using not supported callback instead of throwing error * improve test coverage * bump version * bump version Co-authored-by: Mark Brocato <[email protected]>
1 parent c25cf25 commit 8f4cc9e

File tree

4 files changed

+85
-45
lines changed

4 files changed

+85
-45
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-storefront",
3-
"version": "8.4.0",
3+
"version": "8.5.0",
44
"description": "Build and deploy e-commerce progressive web apps (PWAs) in record time.",
55
"module": "./index.js",
66
"license": "Apache-2.0",

src/LazyHydrate.js

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useEffect, useState, useRef } from 'react'
22
import PropTypes from 'prop-types'
3+
import useIntersectionObserver from './hooks/useIntersectionObserver'
34

45
import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles'
56
import { SheetsRegistry } from 'jss'
@@ -52,18 +53,6 @@ const isBrowser = () => {
5253
)
5354
}
5455

55-
// Used for detecting when the wrapped component becomes visible
56-
const io =
57-
isBrowser() && IntersectionObserver
58-
? new IntersectionObserver(entries => {
59-
entries.forEach(entry => {
60-
if (entry.isIntersecting || entry.intersectionRatio > 0) {
61-
entry.target.dispatchEvent(new CustomEvent('visible'))
62-
}
63-
})
64-
})
65-
: null
66-
6756
function LazyHydrateInstance({ className, ssrOnly, children, on, index, ...props }) {
6857
function isHydrated() {
6958
if (isBrowser()) {
@@ -77,40 +66,52 @@ function LazyHydrateInstance({ className, ssrOnly, children, on, index, ...props
7766
const childRef = useRef(null)
7867
const [hydrated, setHydrated] = useState(isHydrated())
7968

69+
function hydrate() {
70+
setHydrated(true)
71+
// Remove the server side generated stylesheet
72+
const stylesheet = window.document.getElementById(`jss-lazy-${index}`)
73+
if (stylesheet) {
74+
stylesheet.remove()
75+
}
76+
}
77+
8078
useEffect(() => {
8179
setHydrated(isHydrated())
8280
}, [props.hydrated, ssrOnly])
8381

82+
if (on === 'visible') {
83+
useIntersectionObserver(
84+
// As root node does not have any box model, it cannot intersect.
85+
() => childRef.current.children[0],
86+
(visible, disconnect) => {
87+
if (visible) {
88+
hydrate()
89+
disconnect()
90+
}
91+
},
92+
[],
93+
// Fallback to eager hydration
94+
() => {
95+
hydrate()
96+
},
97+
)
98+
}
99+
84100
useEffect(() => {
85101
if (hydrated) return
86102

87-
function hydrate() {
88-
setHydrated(true)
89-
// Remove the server side generated stylesheet
90-
const stylesheet = window.document.getElementById(`jss-lazy-${index}`)
91-
if (stylesheet) {
92-
stylesheet.remove()
93-
}
94-
}
95-
96-
let el
97-
if (on === 'visible') {
98-
if (io && childRef.current.childElementCount) {
99-
// As root node does not have any box model, it cannot intersect.
100-
el = childRef.current.children[0]
101-
io.observe(el)
102-
}
103+
if (on === 'click') {
104+
childRef.current.addEventListener('click', hydrate, {
105+
once: true,
106+
capture: true,
107+
passive: true,
108+
})
103109
}
104110

105-
childRef.current.addEventListener(on, hydrate, {
106-
once: true,
107-
capture: true,
108-
passive: true,
109-
})
110-
111111
return () => {
112-
if (el) io.unobserve(el)
113-
childRef.current.removeEventListener(on, hydrate)
112+
if (on === 'click') {
113+
childRef.current.removeEventListener('click', hydrate)
114+
}
114115
}
115116
}, [hydrated, on])
116117

src/hooks/useIntersectionObserver.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import React, { useEffect } from 'react'
1+
import { useEffect } from 'react'
2+
3+
function getElement(ref) {
4+
if (ref && ref.current) {
5+
return ref.current
6+
}
7+
return ref
8+
}
29

310
/**
411
* Calls a provided callback when the provided element moves into or out of the viewport.
@@ -25,21 +32,25 @@ import React, { useEffect } from 'react'
2532
*
2633
* ```
2734
*
28-
* @param {Function} getRef A function that returns a ref pointing to the element to observe
35+
* @param {Function} getRef A function that returns a ref pointing to the element to observe OR the element itself
2936
* @param {Function} cb A callback to call when visibility changes
3037
* @param {Object[]} deps The IntersectionObserver will be updated to observe a new ref whenever any of these change
38+
* @param {Function} notSupportedCallback Callback fired when IntersectionObserver is not supported
3139
*/
32-
export default function useIntersectionObserver(getRef, cb, deps) {
40+
export default function useIntersectionObserver(getRef, cb, deps, notSupportedCallback) {
3341
useEffect(() => {
42+
if (!window.IntersectionObserver) {
43+
notSupportedCallback &&
44+
notSupportedCallback(new Error('IntersectionObserver is not available'))
45+
return
46+
}
3447
const observer = new IntersectionObserver(entries => {
3548
// if intersectionRatio is 0, the element is out of view and we do not need to do anything.
3649
cb(entries[0].intersectionRatio > 0, () => observer.disconnect())
3750
})
38-
39-
const ref = getRef()
40-
41-
if (ref && ref.current) {
42-
observer.observe(ref.current)
51+
const el = getElement(getRef())
52+
if (el) {
53+
observer.observe(el)
4354
return () => observer.disconnect()
4455
}
4556
}, deps)

test/hooks/useIntersectionObserver.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,32 @@ describe('useIntersectionObserver', () => {
4545
mount(<Test />)
4646
}).not.toThrowError()
4747
})
48+
49+
describe('when IntersectionObserver is not supported', () => {
50+
let IntersectionObserver
51+
52+
beforeEach(() => {
53+
IntersectionObserver = window.IntersectionObserver
54+
delete window.IntersectionObserver
55+
})
56+
57+
afterEach(() => {
58+
window.IntersectionObserver = IntersectionObserver
59+
})
60+
61+
it('should call the not supported callback', () => {
62+
const notSupported = jest.fn()
63+
64+
const Test = () => {
65+
useIntersectionObserver(() => null, jest.fn(), [], notSupported)
66+
return <div />
67+
}
68+
69+
expect(() => {
70+
mount(<Test />)
71+
}).not.toThrowError()
72+
73+
expect(notSupported).toHaveBeenCalled()
74+
})
75+
})
4876
})

0 commit comments

Comments
 (0)