From 8f4cc9ec7c47f5d0095ec9a0a7368a7ddc710733 Mon Sep 17 00:00:00 2001 From: Richard van der Dys Date: Tue, 2 Jun 2020 15:18:03 +0300 Subject: [PATCH] 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 --- package.json | 2 +- src/LazyHydrate.js | 73 +++++++++++----------- src/hooks/useIntersectionObserver.js | 27 +++++--- test/hooks/useIntersectionObserver.test.js | 28 +++++++++ 4 files changed, 85 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index b7a3f901..a7f4c0b7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/LazyHydrate.js b/src/LazyHydrate.js index 2e744582..8ee8047a 100644 --- a/src/LazyHydrate.js +++ b/src/LazyHydrate.js @@ -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' @@ -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()) { @@ -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]) diff --git a/src/hooks/useIntersectionObserver.js b/src/hooks/useIntersectionObserver.js index adae7866..ab9409d7 100644 --- a/src/hooks/useIntersectionObserver.js +++ b/src/hooks/useIntersectionObserver.js @@ -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. @@ -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) diff --git a/test/hooks/useIntersectionObserver.test.js b/test/hooks/useIntersectionObserver.test.js index a02533fe..8adf7285 100644 --- a/test/hooks/useIntersectionObserver.test.js +++ b/test/hooks/useIntersectionObserver.test.js @@ -45,4 +45,32 @@ describe('useIntersectionObserver', () => { mount() }).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
+ } + + expect(() => { + mount() + }).not.toThrowError() + + expect(notSupported).toHaveBeenCalled() + }) + }) })