Skip to content

Commit

Permalink
Handling no IntersectionObserver (#90)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
dijs and markbrocato authored Jun 2, 2020
1 parent c25cf25 commit 8f4cc9e
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 45 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-storefront",
"version": "8.4.0",
"version": "8.5.0",
"description": "Build and deploy e-commerce progressive web apps (PWAs) in record time.",
"module": "./index.js",
"license": "Apache-2.0",
Expand Down
73 changes: 37 additions & 36 deletions src/LazyHydrate.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useState, useRef } from 'react'
import PropTypes from 'prop-types'
import useIntersectionObserver from './hooks/useIntersectionObserver'

import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles'
import { SheetsRegistry } from 'jss'
Expand Down Expand Up @@ -52,18 +53,6 @@ const isBrowser = () => {
)
}

// Used for detecting when the wrapped component becomes visible
const io =
isBrowser() && IntersectionObserver
? new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting || entry.intersectionRatio > 0) {
entry.target.dispatchEvent(new CustomEvent('visible'))
}
})
})
: null

function LazyHydrateInstance({ className, ssrOnly, children, on, index, ...props }) {
function isHydrated() {
if (isBrowser()) {
Expand All @@ -77,40 +66,52 @@ function LazyHydrateInstance({ className, ssrOnly, children, on, index, ...props
const childRef = useRef(null)
const [hydrated, setHydrated] = useState(isHydrated())

function hydrate() {
setHydrated(true)
// Remove the server side generated stylesheet
const stylesheet = window.document.getElementById(`jss-lazy-${index}`)
if (stylesheet) {
stylesheet.remove()
}
}

useEffect(() => {
setHydrated(isHydrated())
}, [props.hydrated, ssrOnly])

if (on === 'visible') {
useIntersectionObserver(
// As root node does not have any box model, it cannot intersect.
() => childRef.current.children[0],
(visible, disconnect) => {
if (visible) {
hydrate()
disconnect()
}
},
[],
// Fallback to eager hydration
() => {
hydrate()
},
)
}

useEffect(() => {
if (hydrated) return

function hydrate() {
setHydrated(true)
// Remove the server side generated stylesheet
const stylesheet = window.document.getElementById(`jss-lazy-${index}`)
if (stylesheet) {
stylesheet.remove()
}
}

let el
if (on === 'visible') {
if (io && childRef.current.childElementCount) {
// As root node does not have any box model, it cannot intersect.
el = childRef.current.children[0]
io.observe(el)
}
if (on === 'click') {
childRef.current.addEventListener('click', hydrate, {
once: true,
capture: true,
passive: true,
})
}

childRef.current.addEventListener(on, hydrate, {
once: true,
capture: true,
passive: true,
})

return () => {
if (el) io.unobserve(el)
childRef.current.removeEventListener(on, hydrate)
if (on === 'click') {
childRef.current.removeEventListener('click', hydrate)
}
}
}, [hydrated, on])

Expand Down
27 changes: 19 additions & 8 deletions src/hooks/useIntersectionObserver.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, { useEffect } from 'react'
import { useEffect } from 'react'

function getElement(ref) {
if (ref && ref.current) {
return ref.current
}
return ref
}

/**
* Calls a provided callback when the provided element moves into or out of the viewport.
Expand All @@ -25,21 +32,25 @@ import React, { useEffect } from 'react'
*
* ```
*
* @param {Function} getRef A function that returns a ref pointing to the element to observe
* @param {Function} getRef A function that returns a ref pointing to the element to observe OR the element itself
* @param {Function} cb A callback to call when visibility changes
* @param {Object[]} deps The IntersectionObserver will be updated to observe a new ref whenever any of these change
* @param {Function} notSupportedCallback Callback fired when IntersectionObserver is not supported
*/
export default function useIntersectionObserver(getRef, cb, deps) {
export default function useIntersectionObserver(getRef, cb, deps, notSupportedCallback) {
useEffect(() => {
if (!window.IntersectionObserver) {
notSupportedCallback &&
notSupportedCallback(new Error('IntersectionObserver is not available'))
return
}
const observer = new IntersectionObserver(entries => {
// if intersectionRatio is 0, the element is out of view and we do not need to do anything.
cb(entries[0].intersectionRatio > 0, () => observer.disconnect())
})

const ref = getRef()

if (ref && ref.current) {
observer.observe(ref.current)
const el = getElement(getRef())
if (el) {
observer.observe(el)
return () => observer.disconnect()
}
}, deps)
Expand Down
28 changes: 28 additions & 0 deletions test/hooks/useIntersectionObserver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,32 @@ describe('useIntersectionObserver', () => {
mount(<Test />)
}).not.toThrowError()
})

describe('when IntersectionObserver is not supported', () => {
let IntersectionObserver

beforeEach(() => {
IntersectionObserver = window.IntersectionObserver
delete window.IntersectionObserver
})

afterEach(() => {
window.IntersectionObserver = IntersectionObserver
})

it('should call the not supported callback', () => {
const notSupported = jest.fn()

const Test = () => {
useIntersectionObserver(() => null, jest.fn(), [], notSupported)
return <div />
}

expect(() => {
mount(<Test />)
}).not.toThrowError()

expect(notSupported).toHaveBeenCalled()
})
})
})

0 comments on commit 8f4cc9e

Please sign in to comment.