import type { Accessor, JSX } from 'solid-js' import { action, untracked } from '@wovin/core/mobx' import { Logger } from 'besonders-logger' import classNames from 'classnames' import { createEffect, createSignal, For, on, onCleanup, Show, untrack } from 'solid-js' import { Spinner } from '../components/mini-components' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars interface LazyRenderProps { items: Accessor initial?: number fallback?: JSX.Element reset?: any children?: (item: T, isInitial: boolean) => JSX.Element allLoaded: Accessor loadMore: () => Promise } // ? & JSX.HTMLAttributes const INITIAL_ITEMS_TO_LOAD = 7 const NEXT_ITEMS_TO_LOAD = 3 export const LazyRender: ( props: LazyRenderProps, ) => JSX.Element = (props) => { const untrackedItems = untrack(() => untracked(() => [...props.items()])) // We observe the items later const { initial = INITIAL_ITEMS_TO_LOAD } = props DEBUG(`.create`, { untrackedItems, initial }) // const [props, otherProps] = splitProps(fullProps, ['items', 'children', 'fallback', 'initial', 'reset']) const [loadedItems, setLoadedItems] = createSignal(untrackedItems.slice(0, initial)) const [skipAnimationFor, setSkipAnimationFor] = createSignal(untrackedItems.slice(0, initial)) const [allLoadedLocal, setAllLoadedLocal] = createSignal(untrackedItems.length <= initial) const [showLoader, setShowLoader] = createSignal(false) const allLoaded = () => allLoadedLocal() && (props.allLoaded ? props.allLoaded() : true) createEffect(on(() => props.reset, () => { DEBUG(`[LazyRender] reset?`, props.reset, { all: allLoaded(), loaded: loadedItems().length, initial }) if (!allLoaded() && loadedItems().length > initial) { LOG(`[LazyRender] reset!`, props.reset, { all: allLoaded(), loaded: loadedItems().length, initial }) setLoadedItems(props.items().slice(0, initial)) } }, { defer: true })) let loaderVisible = false let externalLoadMorePending = false let loadMoreTimer = null const loadMore = action(() => { DEBUG(`[loadMore]`, { allLoadedLocal: allLoadedLocal(), loaded: loadedItems().length, all: props.items().length, allLoadedExt: props.allLoaded?.(), }) if (allLoadedLocal() && props.allLoaded && !props.allLoaded?.()) { DEBUG(`[loadMore] calling external loadMore`, props.loadMore, externalLoadMorePending) if (externalLoadMorePending) return externalLoadMorePending = true void (async () => { const foundMore = await props.loadMore() DEBUG(`[loadMore] external loadMore done. foundMore? ${foundMore}`) externalLoadMorePending = false if (foundMore) { setTimerToCheckLoader(0) // HACK: defer to wait for items in list } })() } else { addChunkToLoadedItems() if (loadedItems().length >= props.items().length) { // observer.disconnect() // ? Stop observing once all items are loaded - how to reconnect setAllLoadedLocal(true) } if (!allLoaded()) { setTimerToCheckLoader() } } function setTimerToCheckLoader(sleep = 250) { loadMoreTimer = setTimeout(() => { DEBUG(`[loadMore] loadMore timer done. still visible?`, loaderVisible) if (!loaderVisible) setShowLoader(false) else loadMore() }, sleep) } function addChunkToLoadedItems() { setLoadedItems((prev) => { // if (prev.length > 20) return prev const nextItems = props.items().slice(prev.length, prev.length + NEXT_ITEMS_TO_LOAD) DEBUG(`[loadMore] Loading ${nextItems.length} more, previously ${prev.length}`, { prev, nextItems }) return nextItems.length ? [...prev, ...nextItems] : prev }) } }) const onObserverStateChange = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { const isVisible = !!entries.find(({ isIntersecting }) => isIntersecting) DEBUG(`[LazyRender] observer state change`, isVisible, entries, observer) if (!isVisible || allLoaded()) { loaderVisible = false setShowLoader(false) return } loaderVisible = true setShowLoader(true) loadMore() } onCleanup(() => loadMoreTimer && clearTimeout(loadMoreTimer)) createEffect(on( () => props.items(), (items) => { LOG('[LazyRender] items changed - re-rendering', { loadedItems: loadedItems() }) const newItems = items.slice(0, loadedItems().length) setLoadedItems(newItems) setSkipAnimationFor(newItems) setAllLoadedLocal(newItems.length >= items.length) setShowLoader(allLoaded()) }, { defer: true }, )) const observer = new IntersectionObserver(onObserverStateChange, { rootMargin: '200px', // Load items when they come within X pixels of the viewport root: document.getElementById('main-container'), }) onCleanup(() => { observer.disconnect() }) return ( <> {(item) => { VERBOSE(`Rendering`, item) return props.children(item, skipAnimationFor().includes(item)) // return
{stringify(item)}
}}
{ DEBUG(`[LazyRender] observer ref`, elem) observer.observe(elem) }} > {' '} Loading more...
) }