diff --git a/src/LazyHydrate.js b/src/LazyHydrate.js
new file mode 100644
index 00000000..2e744582
--- /dev/null
+++ b/src/LazyHydrate.js
@@ -0,0 +1,164 @@
+import React, { useEffect, useState, useRef } from 'react'
+import PropTypes from 'prop-types'
+
+import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles'
+import { SheetsRegistry } from 'jss'
+
+let registries = []
+const generateClassName = createGenerateClassName()
+
+export function clearLazyHydrateRegistries() {
+ registries = []
+}
+
+/*
+ This component renders the server side rendered stylesheets for the
+ lazy hydrated components. Once they become hydrated, these stylesheets
+ will be removed.
+*/
+export function LazyStyleElements() {
+ return (
+ <>
+ {registries.map((registry, index) => {
+ const id = `jss-lazy-${index}`
+ return
+ })}
+ >
+ )
+}
+
+function LazyStylesProvider({ children }) {
+ const registry = new SheetsRegistry()
+ registries.push(registry)
+ return (
+
+ {children}
+
+ )
+}
+
+const isBrowser = () => {
+ if (process.env.NODE_ENV === 'test') {
+ return process.env.IS_BROWSER === 'true'
+ }
+ return (
+ typeof window !== 'undefined' &&
+ typeof window.document !== 'undefined' &&
+ typeof window.document.createElement !== 'undefined'
+ )
+}
+
+// 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()) {
+ if (ssrOnly) return false
+ return props.hydrated
+ } else {
+ return true
+ }
+ }
+
+ const childRef = useRef(null)
+ const [hydrated, setHydrated] = useState(isHydrated())
+
+ useEffect(() => {
+ setHydrated(isHydrated())
+ }, [props.hydrated, ssrOnly])
+
+ 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)
+ }
+ }
+
+ childRef.current.addEventListener(on, hydrate, {
+ once: true,
+ capture: true,
+ passive: true,
+ })
+
+ return () => {
+ if (el) io.unobserve(el)
+ childRef.current.removeEventListener(on, hydrate)
+ }
+ }, [hydrated, on])
+
+ if (hydrated) {
+ return (
+
+ {children}
+
+ )
+ } else {
+ return (
+
+ )
+ }
+}
+
+/**
+ * LazyHydrate
+ *
+ * Example:
+ *
+ *
+ * some expensive component
+ *
+ *
+ */
+
+function LazyHydrate({ children, ...props }) {
+ return (
+
+ {children}
+
+ )
+}
+
+LazyHydrate.propTypes = {
+ // Control the hydration of the component externally with this prop
+ hydrated: PropTypes.bool,
+ // Force component to never hydrate
+ ssrOnly: PropTypes.bool,
+ // Event to trigger hydration
+ on: PropTypes.oneOf(['visible', 'click']),
+}
+
+export default LazyHydrate
diff --git a/test/LazyHydrate.test.js b/test/LazyHydrate.test.js
new file mode 100644
index 00000000..93faefa9
--- /dev/null
+++ b/test/LazyHydrate.test.js
@@ -0,0 +1,84 @@
+import React from 'react'
+import { mount } from 'enzyme'
+import LazyHydrate, {
+ clearLazyHydrateRegistries,
+ LazyStyleElements,
+} from 'react-storefront/LazyHydrate'
+
+describe('LazyHydrate', () => {
+ let wrapper
+
+ afterEach(() => {
+ wrapper.unmount()
+ })
+
+ it('should clear registries', () => {
+ const hydrate = mount(
+
+
+ ,
+ )
+ wrapper = mount(
+
+
+
,
+ )
+ // Should render registered lazy styles
+ expect(wrapper.find('style').length).toBe(1)
+ clearLazyHydrateRegistries()
+ // Simulating next page render
+ wrapper = mount(
+
+
+
,
+ )
+ // Should not hold on to old registered styles
+ expect(wrapper.find('style').length).toBe(0)
+ hydrate.unmount()
+ })
+
+ it('should pass event through when hydrated', () => {
+ const click = jest.fn()
+ wrapper = mount(
+
+
+ ,
+ )
+ wrapper.find('button').simulate('click')
+ expect(click).toHaveBeenCalled()
+ })
+
+ it('should render children during SSR only mode', () => {
+ const click = jest.fn()
+ process.env.IS_BROWSER = 'false'
+ wrapper = mount(
+
+
+ ,
+ )
+ expect(wrapper.html()).toContain('')
+ })
+
+ it('should not render children in the browser during SSR only mode', () => {
+ const click = jest.fn()
+ process.env.IS_BROWSER = 'true'
+ wrapper = mount(
+
+
+ ,
+ )
+ expect(wrapper.find('button').length).toBe(0)
+ })
+
+ it('should hydrate in browser once triggered', () => {
+ process.env.IS_BROWSER = 'true'
+ wrapper = mount(
+
+
+ ,
+ )
+ expect(wrapper.html()).not.toContain('')
+ wrapper.setProps({ hydrated: true })
+ expect(wrapper.html()).toContain('')
+ })
+})