1- import * as React from 'react'
1+ import React , { useCallback , useEffect , useLayoutEffect , useMemo , useState } from 'react'
22import { InView } from 'react-intersection-observer'
33
4- export interface IProps {
5- initialShow ?: boolean
6- placeholderHeight ?: number
7- _debug ?: boolean
8- placeholderClassName ?: string
9- width ?: string | number
10- margin ?: string
11- id ?: string | undefined
12- className ?: string
13- }
14-
15- declare global {
16- interface Window {
17- requestIdleCallback (
18- callback : Function ,
19- options ?: {
20- timeout : number
21- }
22- ) : number
23- cancelIdleCallback ( callback : number )
24- }
25- }
26-
274interface IElementMeasurements {
285 width : string | number
296 clientHeight : number
@@ -34,160 +11,172 @@ interface IElementMeasurements {
3411 id : string | undefined
3512}
3613
37- interface IState extends IElementMeasurements {
38- inView : boolean
39- isMeasured : boolean
40- }
41-
4214const OPTIMIZE_PERIOD = 5000
15+ const IDLE_CALLBACK_TIMEOUT = 100
16+
4317/**
4418 * This is a component that allows optimizing the amount of elements present in the DOM through replacing them
4519 * with placeholders when they aren't visible in the viewport.
4620 *
4721 * @export
48- * @class VirtualElement
49- * @extends {React.Component<IProps, IState> }
22+ * @param {(React.PropsWithChildren<{
23+ * initialShow?: boolean
24+ * placeholderHeight?: number
25+ * _debug?: boolean
26+ * placeholderClassName?: string
27+ * width?: string | number
28+ * margin?: string
29+ * id?: string | undefined
30+ * className?: string
31+ * }>)} {
32+ * initialShow,
33+ * placeholderHeight,
34+ * placeholderClassName,
35+ * width,
36+ * margin,
37+ * id,
38+ * className,
39+ * children,
40+ * }
41+ * @return {* } {(JSX.Element | null)}
5042 */
51- export class VirtualElement extends React . Component < React . PropsWithChildren < IProps > , IState > {
52- private el : HTMLElement | null = null
53- private instance : HTMLElement | null = null
54- private optimizeTimeout : NodeJS . Timer | null = null
55- private refreshSizingTimeout : NodeJS . Timer | null = null
56- private styleObj : CSSStyleDeclaration | undefined
57-
58- constructor ( props : IProps ) {
59- super ( props )
60- this . state = {
61- inView : props . initialShow || false ,
62- isMeasured : false ,
63- clientHeight : 0 ,
64- width : 'auto' ,
65- marginBottom : undefined ,
66- marginTop : undefined ,
67- marginLeft : undefined ,
68- marginRight : undefined ,
69- id : undefined ,
43+ export function VirtualElement ( {
44+ initialShow,
45+ placeholderHeight,
46+ placeholderClassName,
47+ width,
48+ margin,
49+ id,
50+ className,
51+ children,
52+ } : React . PropsWithChildren < {
53+ initialShow ?: boolean
54+ placeholderHeight ?: number
55+ _debug ?: boolean
56+ placeholderClassName ?: string
57+ width ?: string | number
58+ margin ?: string
59+ id ?: string | undefined
60+ className ?: string
61+ } > ) : JSX . Element | null {
62+ const [ inView , setInView ] = useState ( initialShow ?? false )
63+ const [ isShowingChildren , setIsShowingChildren ] = useState ( inView )
64+ const [ measurements , setMeasurements ] = useState < IElementMeasurements | null > ( null )
65+ const [ ref , setRef ] = useState < HTMLDivElement | null > ( null )
66+ const [ childRef , setChildRef ] = useState < HTMLElement | null > ( null )
67+
68+ const isMeasured = ! ! measurements
69+
70+ const styleObj = useMemo < React . CSSProperties > (
71+ ( ) => ( {
72+ width : width ?? measurements ?. width ?? 'auto' ,
73+ height : ( measurements ?. clientHeight ?? placeholderHeight ?? '0' ) + 'px' ,
74+ marginTop : measurements ?. marginTop ,
75+ marginLeft : measurements ?. marginLeft ,
76+ marginRight : measurements ?. marginRight ,
77+ marginBottom : measurements ?. marginBottom ,
78+ } ) ,
79+ [ width , measurements , placeholderHeight ]
80+ )
81+
82+ const onVisibleChanged = useCallback ( ( visible : boolean ) => {
83+ setInView ( visible )
84+ } , [ ] )
85+
86+ useEffect ( ( ) => {
87+ if ( inView === true ) {
88+ setIsShowingChildren ( true )
89+ return
7090 }
71- }
7291
73- private visibleChanged = ( inView : boolean ) => {
74- this . props . _debug && console . log ( this . props . id , 'Changed' , inView )
75- if ( this . optimizeTimeout ) {
76- clearTimeout ( this . optimizeTimeout )
77- this . optimizeTimeout = null
78- }
79- if ( inView && ! this . state . inView ) {
80- this . setState ( {
81- inView,
82- } )
83- } else if ( ! inView && this . state . inView ) {
84- this . optimizeTimeout = setTimeout ( ( ) => {
85- this . optimizeTimeout = null
86- const measurements = this . measureElement ( ) || undefined
87- this . setState ( {
88- inView,
89-
90- isMeasured : measurements ? true : false ,
91- ...measurements ,
92- } as IState )
93- } , OPTIMIZE_PERIOD )
94- }
95- }
92+ let idleCallback : number | undefined
93+ const optimizeTimeout = window . setTimeout ( ( ) => {
94+ idleCallback = window . requestIdleCallback (
95+ ( ) => {
96+ if ( childRef ) {
97+ setMeasurements ( measureElement ( childRef ) )
98+ }
99+ setIsShowingChildren ( false )
100+ } ,
101+ {
102+ timeout : IDLE_CALLBACK_TIMEOUT ,
103+ }
104+ )
105+ } , OPTIMIZE_PERIOD )
96106
97- private measureElement = ( ) : IElementMeasurements | null => {
98- if ( this . el ) {
99- const style = this . styleObj || window . getComputedStyle ( this . el )
100- this . styleObj = style
101- this . props . _debug && console . log ( this . props . id , 'Re-measuring child' , this . el . clientHeight )
102-
103- return {
104- width : style . width || 'auto' ,
105- clientHeight : this . el . clientHeight ,
106- marginTop : style . marginTop || undefined ,
107- marginBottom : style . marginBottom || undefined ,
108- marginLeft : style . marginLeft || undefined ,
109- marginRight : style . marginRight || undefined ,
110- id : this . el . id ,
107+ return ( ) => {
108+ if ( idleCallback ) {
109+ window . cancelIdleCallback ( idleCallback )
111110 }
112- }
113-
114- return null
115- }
116111
117- private refreshSizing = ( ) => {
118- this . refreshSizingTimeout = null
119- const measurements = this . measureElement ( )
120- if ( measurements ) {
121- this . setState ( {
122- isMeasured : true ,
123- ...measurements ,
124- } )
112+ window . clearTimeout ( optimizeTimeout )
125113 }
126- }
114+ } , [ childRef , inView ] )
127115
128- private findChildElement = ( ) => {
129- if ( ! this . el || ! this . el . parentElement ) {
130- const el = this . instance ? ( this . instance . firstElementChild as HTMLElement ) : null
131- if ( el && ! el . classList . contains ( 'virtual-element-placeholder' ) ) {
132- this . el = el
133- this . styleObj = undefined
134- this . refreshSizingTimeout = setTimeout ( this . refreshSizing , 250 )
135- }
136- }
137- }
116+ const showPlaceholder = ! isShowingChildren && ( ! initialShow || isMeasured )
138117
139- private setRef = ( instance : HTMLElement | null ) => {
140- this . instance = instance
141- this . findChildElement ( )
142- }
118+ useLayoutEffect ( ( ) => {
119+ if ( ! ref || showPlaceholder ) return
143120
144- componentDidUpdate ( _ : IProps , prevState : IState ) : void {
145- if ( this . state . inView && prevState . inView !== this . state . inView ) {
146- this . findChildElement ( )
147- }
148- }
121+ const el = ref ?. firstElementChild
122+ if ( ! el || el . classList . contains ( 'virtual-element-placeholder' ) || ! ( el instanceof HTMLElement ) ) return
149123
150- componentWillUnmount ( ) : void {
151- if ( this . optimizeTimeout ) clearTimeout ( this . optimizeTimeout )
152- if ( this . refreshSizingTimeout ) clearTimeout ( this . refreshSizingTimeout )
153- }
124+ setChildRef ( el )
154125
155- render ( ) : JSX . Element {
156- this . props . _debug &&
157- console . log (
158- this . props . id ,
159- this . state . inView ,
160- this . props . initialShow ,
161- this . state . isMeasured ,
162- ! this . state . inView && ( ! this . props . initialShow || this . state . isMeasured )
126+ let idleCallback : number | undefined
127+ const refreshSizingTimeout = window . setTimeout ( ( ) => {
128+ idleCallback = window . requestIdleCallback (
129+ ( ) => {
130+ setMeasurements ( measureElement ( el ) )
131+ } ,
132+ {
133+ timeout : IDLE_CALLBACK_TIMEOUT ,
134+ }
163135 )
164- return (
165- < InView
166- threshold = { 0 }
167- rootMargin = { this . props . margin || '50% 0px 50% 0px' }
168- onChange = { this . visibleChanged }
169- className = { this . props . className }
170- as = "div"
171- >
172- < div ref = { this . setRef } >
173- { ! this . state . inView && ( ! this . props . initialShow || this . state . isMeasured ) ? (
174- < div
175- id = { this . state . id || this . props . id }
176- className = { 'virtual-element-placeholder ' + ( this . props . placeholderClassName || '' ) }
177- style = { {
178- width : this . props . width || this . state . width ,
179- height : ( this . state . clientHeight || this . props . placeholderHeight || '0' ) + 'px' ,
180- marginTop : this . state . marginTop ,
181- marginLeft : this . state . marginLeft ,
182- marginRight : this . state . marginRight ,
183- marginBottom : this . state . marginBottom ,
184- } }
185- > </ div >
186- ) : (
187- this . props . children
188- ) }
189- </ div >
190- </ InView >
191- )
136+ } , 1000 )
137+
138+ return ( ) => {
139+ if ( idleCallback ) {
140+ window . cancelIdleCallback ( idleCallback )
141+ }
142+ window . clearTimeout ( refreshSizingTimeout )
143+ }
144+ } , [ ref , showPlaceholder ] )
145+
146+ return (
147+ < InView
148+ threshold = { 0 }
149+ rootMargin = { margin || '50% 0px 50% 0px' }
150+ onChange = { onVisibleChanged }
151+ className = { className }
152+ as = "div"
153+ >
154+ < div ref = { setRef } >
155+ { showPlaceholder ? (
156+ < div
157+ id = { measurements ?. id ?? id }
158+ className = { `virtual-element-placeholder ${ placeholderClassName } ` }
159+ style = { styleObj }
160+ > </ div >
161+ ) : (
162+ children
163+ ) }
164+ </ div >
165+ </ InView >
166+ )
167+ }
168+
169+ function measureElement ( el : HTMLElement ) : IElementMeasurements | null {
170+ const style = window . getComputedStyle ( el )
171+ const clientRect = el . getBoundingClientRect ( )
172+
173+ return {
174+ width : style . width || 'auto' ,
175+ clientHeight : clientRect . height ,
176+ marginTop : style . marginTop || undefined ,
177+ marginBottom : style . marginBottom || undefined ,
178+ marginLeft : style . marginLeft || undefined ,
179+ marginRight : style . marginRight || undefined ,
180+ id : el . id ,
192181 }
193182}
0 commit comments