Skip to content

Commit

Permalink
Refactored LazyHydrate, removed hold prop and added a hydrateManually…
Browse files Browse the repository at this point in the history
… MotionValue
  • Loading branch information
paales committed Dec 5, 2023
1 parent 24bcfbe commit 56acc0e
Show file tree
Hide file tree
Showing 6 changed files with 36 additions and 108 deletions.
4 changes: 2 additions & 2 deletions examples/magento-graphcms/components/GraphCMS/RowRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export function RowRenderer(props: PageProps) {
return (
<>
{content?.map((item, index) => (
<LazyHydrate eager={index < loadingEager}>
<RenderType key={item.id} renderer={mergedRenderer} {...item} />
<LazyHydrate key={item.id} eager={index < loadingEager}>
<RenderType renderer={mergedRenderer} {...item} />
</LazyHydrate>
))}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,9 @@ export function LayoutNavigation(props: LayoutNavigationProps) {
const selection = useNavigationSelection()
const router = useRouter()

const [hold, setHold] = useState(true)
const i = () => selection.set([menu?.items?.[0]?.uid || ''])
return (
<>
<NavigationProvider
hold={hold}
selection={selection}
items={useMemoDeep(
() => [
Expand Down Expand Up @@ -131,12 +128,7 @@ export function LayoutNavigation(props: LayoutNavigationProps) {
</DesktopNavItem>
))}

<DesktopNavItem
onClick={() => {
setHold(false)
setTimeout(() => selection.set([menu?.items?.[0]?.uid || '']), 100)
}}
>
<DesktopNavItem onClick={() => selection.set([menu?.items?.[0]?.uid || ''])}>
{menu?.items?.[0]?.name}
<IconSvg src={iconChevronDown} />
</DesktopNavItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,8 @@ export function ProductListItemsBase(props: ProductItemsGridProps) {
>
{items?.map((item, idx) =>
item ? (
<LazyHydrate eager={loadingEager > idx}>
<LazyHydrate key={item.uid ?? ''} eager={loadingEager > idx}>
<RenderType
key={item.uid ?? ''}
renderer={renderers}
sizes={
size === 'small'
Expand Down
111 changes: 26 additions & 85 deletions packages/next-ui/LazyHydrate/LazyHydrate.tsx
Original file line number Diff line number Diff line change
@@ -1,119 +1,69 @@
import React, {
useState,
useRef,
useEffect,
useLayoutEffect,
useCallback,
startTransition,
} from 'react'
import { MotionValue } from 'framer-motion'
import React, { useState, useRef, useLayoutEffect, startTransition } from 'react'

type WithHydrationOnDemandOptions = {
wrapperProps?: React.ComponentProps<'div'>
forceHydration?: boolean
hold?: boolean
afterHydrate?: () => void
}

function withHydrationOnDemandServerSide<P extends object>(options: WithHydrationOnDemandOptions) {
const { wrapperProps } = options
function withHydrationOnDemandServerSide<P extends object>() {
return (Component: React.ComponentType<P>) => (props: P) => (
<section data-hydration-on-demand {...wrapperProps}>
<section data-lazy-hydrate>
<Component {...props} />
</section>
)
}

function withHydrationOnDemandClientSide<P extends object>(incoming: WithHydrationOnDemandOptions) {
const { wrapperProps, forceHydration = false } = incoming

function withHydrationOnDemandClientSide<P extends object>() {
return (Component: React.ComponentType<P>) => {
function ComponentWithHydration(
props: P & { forceHydration?: boolean; hold?: boolean; afterHydrate?: () => void },
) {
const { hold, afterHydrate } = props
function ComponentWithHydration(props: P & { hydrateManually?: MotionValue<boolean> }) {
const { hydrateManually } = props
const rootRef = useRef<HTMLElement>(null)

const isInputPending = () => {
// @ts-expect-error navigator.scheduling is not defined in the types
const isPending = navigator?.scheduling?.isInputPending?.()
return isPending ?? true
}

const getDefaultHydrationState = () => {
const isNotInputPending = false && !isInputPending()
return isNotInputPending || forceHydration
}

const [isHydrated, setIsHydrated] = useState(getDefaultHydrationState())

const hydrate = useCallback(() => {
startTransition(() => {
setIsHydrated(true)
if (afterHydrate) afterHydrate()
})
}, [afterHydrate])
const [isHydrated, setIsHydrated] = useState(hydrateManually?.get())

useLayoutEffect(() => {
if (hold) return

if (isHydrated) return

if (forceHydration) {
hydrate()
return
// If we are manually hydrating, we watch that value and do not use the IntersectionObserver
if (isHydrated || !rootRef.current) return undefined
const wasRenderedServerSide = rootRef.current?.hasAttribute('data-lazy-hydrate')
if (!wasRenderedServerSide) {
setIsHydrated(true)
return undefined
}

const wasRenderedServerSide = !!rootRef.current?.getAttribute('data-hydration-on-demand')
const shouldHydrate = !wasRenderedServerSide && !false

if (shouldHydrate) hydrate()
}, [forceHydration, hold])

useEffect(() => {
if (isHydrated || !rootRef.current || hold) return undefined
hydrateManually?.on('change', (val) => val && setIsHydrated(val))
if (hydrateManually) return undefined

const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && entry.intersectionRatio > 0) {
return hydrate()
startTransition(() => setIsHydrated(true))
}
},
{ rootMargin: '200px' },
)

observer.observe(rootRef.current)

return () => observer.disconnect()
}, [hydrate, isHydrated])
}, [hydrateManually, isHydrated])

return !isHydrated ? (
<section
ref={rootRef}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: '' }}
suppressHydrationWarning
{...wrapperProps}
/>
) : (
<section {...wrapperProps}>
<section>
<Component {...props} />
</section>
)
}

if (process.env.NODE_ENV !== 'production') {
ComponentWithHydration.displayName = `withHydrationOnDemand(${
Component.displayName ?? Component.name ?? 'Component'
})`
}

return ComponentWithHydration
}
}

export function lazyHydrate<P extends object>(Component: React.ComponentType<P>) {
return typeof window !== 'undefined'
? withHydrationOnDemandClientSide<P>({})(Component)
: withHydrationOnDemandServerSide<P>({})(Component)
? withHydrationOnDemandClientSide<P>()(Component)
: withHydrationOnDemandServerSide<P>()(Component)
}

export type LazyHydrateProps = {
Expand All @@ -122,21 +72,12 @@ export type LazyHydrateProps = {
* When eager is set to true, it disables all functionality and render the component regularly
*/
eager?: boolean
/**
* When hold is set to true, render nothing
*/
hold?: boolean

afterHydrate?: () => void
hydrateManually?: MotionValue<boolean>
}

export function LazyHydrate(props: {
children: React.ReactNode
eager?: boolean
hold?: boolean
afterHydrate?: () => void
}) {
const { children, eager = false, hold = false, afterHydrate } = props
export function LazyHydrate(props: LazyHydrateProps) {
const { children, eager = false, hydrateManually } = props
const LazyComponent = lazyHydrate(() => children)
return eager ? children : <LazyComponent hold={hold} afterHydrate={afterHydrate} />
return eager ? children : <LazyComponent hydrateManually={hydrateManually} />
}
4 changes: 0 additions & 4 deletions packages/next-ui/Navigation/components/NavigationOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,6 @@ export const NavigationOverlay = React.memo((props: NavigationOverlayProps) => {
c ? false : s !== false,
)

useEffect(() => {
animating.set(true)
}, [activeAndNotClosing, animating])

const afterClose = useEventCallback(() => {
if (!closing.get()) return
setTimeout(() => {
Expand Down
12 changes: 6 additions & 6 deletions packages/next-ui/Navigation/components/NavigationProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { MotionConfig, useMotionValue } from 'framer-motion'
import { MotionConfig, useMotionValue, useTransform } from 'framer-motion'
import React, { useMemo } from 'react'
import { isElement } from 'react-is'
import { LazyHydrate } from '../../LazyHydrate'
import { nonNullable } from '../../RenderType/nonNullable'
import {
NavigationNode,
NavigationContextType,
Expand All @@ -22,7 +23,6 @@ export type NavigationProviderBaseProps = {
}

export type NavigationProviderProps = NavigationProviderBaseProps & { hold?: boolean }
const nonNullable = <T,>(value: T): value is NonNullable<T> => value !== null && value !== undefined

const NavigationProviderBase = React.memo<NavigationProviderBaseProps>((props) => {
const {
Expand All @@ -38,8 +38,6 @@ const NavigationProviderBase = React.memo<NavigationProviderBaseProps>((props) =
const animating = useMotionValue(false)
const closing = useMotionValue(false)

console.log('joe')

const value = useMemo<NavigationContextType>(
() => ({
hideRootOnNavigate,
Expand Down Expand Up @@ -79,9 +77,11 @@ const NavigationProviderBase = React.memo<NavigationProviderBaseProps>((props) =
})

export function NavigationProvider(props: NavigationProviderProps) {
const { hold = false } = props
const { selection } = props
const hydrateManually = useTransform(selection, (s) => s !== false)

return (
<LazyHydrate hold={hold} afterHydrate={}>
<LazyHydrate hydrateManually={hydrateManually}>
<NavigationProviderBase {...props} />
</LazyHydrate>
)
Expand Down

0 comments on commit 56acc0e

Please sign in to comment.