@@ -5,47 +5,33 @@ import { isBrowser, isDev } from "./constants.macro";
55export type LazyProps = {
66 ssrOnly ?: boolean ;
77 whenIdle ?: boolean ;
8- whenVisible ?: boolean ;
9- noWrapper ?: boolean ;
8+ whenVisible ?: boolean | IntersectionObserverInit ;
9+ noWrapper ?: boolean | keyof JSX . IntrinsicElements ;
1010 didHydrate ?: VoidFunction ;
1111 promise ?: Promise < any > ;
1212 on ?: ( keyof HTMLElementEventMap ) [ ] | keyof HTMLElementEventMap ;
1313 children : React . ReactElement ;
1414} ;
1515
16- type Props = Omit < React . HTMLProps < HTMLDivElement > , "dangerouslySetInnerHTML" > &
16+ type Props = Omit < React . HTMLProps < HTMLElement > , "dangerouslySetInnerHTML" > &
1717 LazyProps ;
1818
1919type VoidFunction = ( ) => void ;
2020
21- const event = "hydrate" ;
22-
23- const io =
24- isBrowser && typeof IntersectionObserver !== "undefined"
25- ? new IntersectionObserver (
26- entries => {
27- entries . forEach ( entry => {
28- if ( entry . isIntersecting || entry . intersectionRatio > 0 ) {
29- entry . target . dispatchEvent ( new CustomEvent ( event ) ) ;
30- }
31- } ) ;
32- } ,
33- {
34- rootMargin : "250px"
35- }
36- )
37- : null ;
38-
3921// React currently throws a warning when using useLayoutEffect on the server.
4022const useIsomorphicLayoutEffect = isBrowser
4123 ? React . useLayoutEffect
4224 : React . useEffect ;
4325
26+ function reducer ( ) {
27+ return true ;
28+ }
29+
4430function LazyHydrate ( props : Props ) {
45- const childRef = React . useRef < HTMLDivElement > ( null ) ;
31+ const childRef = React . useRef < HTMLElement > ( null ) ;
4632
4733 // Always render on server
48- const [ hydrated , setHydrated ] = React . useState ( ! isBrowser ) ;
34+ const [ hydrated , hydrate ] = React . useReducer ( reducer , ! isBrowser ) ;
4935
5036 const {
5137 noWrapper,
@@ -76,27 +62,63 @@ function LazyHydrate(props: Props) {
7662 useIsomorphicLayoutEffect ( ( ) => {
7763 // No SSR Content
7864 if ( ! childRef . current . hasChildNodes ( ) ) {
79- setHydrated ( true ) ;
65+ hydrate ( ) ;
8066 }
8167 } , [ ] ) ;
8268
69+ React . useEffect ( ( ) => {
70+ if ( hydrated && didHydrate ) {
71+ didHydrate ( ) ;
72+ }
73+ // eslint-disable-next-line react-hooks/exhaustive-deps
74+ } , [ hydrated ] ) ;
75+
8376 React . useEffect ( ( ) => {
8477 if ( ssrOnly || hydrated ) return ;
78+ const rootElement = childRef . current ;
79+
8580 const cleanupFns : VoidFunction [ ] = [ ] ;
8681 function cleanup ( ) {
87- while ( cleanupFns . length ) {
88- cleanupFns . pop ( ) ( ) ;
89- }
90- }
91- function hydrate ( ) {
92- setHydrated ( true ) ;
93- if ( didHydrate ) didHydrate ( ) ;
82+ cleanupFns . forEach ( fn => {
83+ fn ( ) ;
84+ } ) ;
9485 }
9586
9687 if ( promise ) {
97- promise . then ( hydrate ) . catch ( hydrate ) ;
88+ promise . then ( hydrate , hydrate ) ;
9889 }
9990
91+ if ( whenVisible ) {
92+ const element = noWrapper
93+ ? rootElement
94+ : // As root node does not have any box model, it cannot intersect.
95+ rootElement . firstElementChild ;
96+
97+ if ( element && typeof IntersectionObserver !== "undefined" ) {
98+ const observerOptions =
99+ typeof whenVisible === "object"
100+ ? whenVisible
101+ : {
102+ rootMargin : "250px"
103+ } ;
104+
105+ const io = new IntersectionObserver ( entries => {
106+ entries . forEach ( entry => {
107+ if ( entry . isIntersecting || entry . intersectionRatio > 0 ) {
108+ hydrate ( ) ;
109+ }
110+ } ) ;
111+ } , observerOptions ) ;
112+
113+ io . observe ( element ) ;
114+
115+ cleanupFns . push ( ( ) => {
116+ io . disconnect ( ) ;
117+ } ) ;
118+ } else {
119+ return hydrate ( ) ;
120+ }
121+ }
100122 if ( whenIdle ) {
101123 // @ts -ignore
102124 if ( typeof requestIdleCallback !== "undefined" ) {
@@ -114,53 +136,49 @@ function LazyHydrate(props: Props) {
114136 }
115137 }
116138
117- let events = Array . isArray ( on ) ? on . slice ( ) : [ on ] ;
118-
119- if ( whenVisible ) {
120- if ( io && childRef . current . childElementCount ) {
121- // As root node does not have any box model, it cannot intersect.
122- const el = childRef . current . children [ 0 ] ;
123- io . observe ( el ) ;
124- events . push ( event as keyof HTMLElementEventMap ) ;
125-
126- cleanupFns . push ( ( ) => {
127- io . unobserve ( el ) ;
128- } ) ;
129- } else {
130- return hydrate ( ) ;
131- }
132- }
139+ const events = ( [ ] as Array < keyof HTMLElementEventMap > ) . concat ( on ) ;
133140
134141 events . forEach ( event => {
135- childRef . current . addEventListener ( event , hydrate , {
142+ rootElement . addEventListener ( event , hydrate , {
136143 once : true ,
137- capture : true ,
138144 passive : true
139145 } ) ;
140146 cleanupFns . push ( ( ) => {
141- childRef . current . removeEventListener ( event , hydrate , { capture : true } ) ;
147+ rootElement . removeEventListener ( event , hydrate , { } ) ;
142148 } ) ;
143149 } ) ;
144150
145151 return cleanup ;
146- } , [ hydrated , on , ssrOnly , whenIdle , whenVisible , didHydrate , promise ] ) ;
152+ } , [
153+ hydrated ,
154+ on ,
155+ ssrOnly ,
156+ whenIdle ,
157+ whenVisible ,
158+ didHydrate ,
159+ promise ,
160+ noWrapper
161+ ] ) ;
162+
163+ const WrapperElement = ( ( typeof noWrapper === "string"
164+ ? noWrapper
165+ : "div" ) as unknown ) as React . FC < React . HTMLProps < HTMLElement > > ;
147166
148167 if ( hydrated ) {
149168 if ( noWrapper ) {
150169 return children ;
151170 }
152171 return (
153- < div ref = { childRef } style = { { display : "contents" } } { ...rest } >
172+ < WrapperElement ref = { childRef } style = { { display : "contents" } } { ...rest } >
154173 { children }
155- </ div >
174+ </ WrapperElement >
156175 ) ;
157176 } else {
158177 return (
159- < div
178+ < WrapperElement
179+ { ...rest }
160180 ref = { childRef }
161- style = { { display : "contents" } }
162181 suppressHydrationWarning
163- { ...rest }
164182 dangerouslySetInnerHTML = { { __html : "" } }
165183 />
166184 ) ;
0 commit comments