Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
})
})
})