refactor(island): ♻️ extract universal CardList from specific BlogCardList

This commit is contained in:
Julien Oculi 2024-07-10 14:46:01 +02:00
parent 8b716a382f
commit 80ad700c4c
4 changed files with 129 additions and 79 deletions

View file

@ -1,75 +1,38 @@
import { BlogCard, BlogProps } from ':components/BlogBlocks.tsx'
import Suspense from ':islands/Suspens.tsx'
import { requestApiStream } from ':src/utils.ts'
import { Signal, useSignal } from '@preact/signals'
import type { JSX, Ref } from 'preact'
import { useEffect, useRef } from 'preact/hooks'
function fillList(
list: Signal<JSX.Element[]>,
{ limit, ac }: { limit?: number; ac?: AbortController },
) {
;(async () => {
const newsList = requestApiStream<void, BlogProps>(
'news/fetchAll',
'GET',
)
for await (const news of newsList) {
list.value = [
...list.value,
BlogCard({ ...news, lastUpdate: new Date(news.lastUpdate) }),
]
if (limit && list.value.length >= limit) break
}
ac?.abort()
})()
}
import CardList from ':islands/CardList.tsx'
import type { Ref } from 'preact'
export default function BlogCardList(
{ limit, usePlaceholder, useObserver }: {
usePlaceholder?: boolean
limit?: number
usePlaceholder?: boolean
useObserver?: boolean
},
) {
const list: Signal<JSX.Element[]> = useSignal<JSX.Element[]>([])
const ac = new AbortController()
const ref = useRef(null)
useEffect(() => {
if (ref.current && useObserver) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
fillList(list, { limit, ac })
observer.disconnect()
}
}, {
rootMargin: '300px',
})
//@ts-expect-error Need to investigate why it's work
observer.observe(ref.current.base)
} else {
fillList(list, { limit, ac })
}
})
if (limit && usePlaceholder) {
const placeholders = Array
.from({ length: limit })
.map((_, index) => (
<Suspense
loader={<Placeholder ref={index === 0 ? ref : undefined} />}
fallback={Fallback}
signal={ac.signal}
>
{updateFromList(list, index)}
</Suspense>
))
return <>{placeholders}</>
if (usePlaceholder) {
return (
<CardList
apiRoute='news/fetchAll'
builder={builder}
limit={limit}
placeholder={Placeholder}
fallback={Fallback}
useObserver={useObserver}
/>
)
}
return <>{list}</>
return (
<CardList
apiRoute='news/fetchAll'
builder={builder}
limit={limit}
/>
)
}
function builder(news: BlogProps) {
return BlogCard({ ...news, lastUpdate: new Date(news.lastUpdate) })
}
function Placeholder({ ref }: { ref?: Ref<HTMLDivElement> | undefined }) {
@ -93,18 +56,3 @@ function Fallback() {
</div>
)
}
function updateFromList(
list: Signal<JSX.Element[]>,
index: number,
): Promise<JSX.Element> {
const { promise, resolve } = Promise.withResolvers<JSX.Element>()
list.subscribe((value: JSX.Element[]) => {
const selected = value.at(index)
if (selected) {
resolve(selected)
}
})
return promise
}

102
islands/CardList.tsx Normal file
View file

@ -0,0 +1,102 @@
import Suspense, { Fallback } from ':islands/Suspens.tsx'
import { requestApiStream } from ':src/utils.ts'
import { Signal, useSignal } from '@preact/signals'
import type { JSX, Ref } from 'preact'
import { useEffect, useRef } from 'preact/hooks'
export type Builder<T> = (props: T) => JSX.Element
export type CardListProps<ApiResponse, RefType = null> = {
apiRoute: string
builder: Builder<ApiResponse>
limit?: number
} | {
apiRoute: string
builder: Builder<ApiResponse>
limit?: number
useObserver?: boolean
placeholder: ({ ref }: { ref: Ref<RefType> | undefined }) => JSX.Element
fallback: Fallback
}
export default function CardList<ApiResponse, RefType = null>(
{ limit, builder, ...props }: CardListProps<ApiResponse, RefType>,
) {
const list: Signal<JSX.Element[]> = useSignal<JSX.Element[]>([])
const ac = new AbortController()
const ref = useRef(null)
const useObserver = 'useObserver' in props ? props.useObserver : false
const placeholder = 'placeholder' in props ? props.placeholder : false
const fallback = 'fallback' in props ? props.fallback : false
useEffect(() => {
if (ref.current && useObserver) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
fillList(list, builder, { limit, ac })
observer.disconnect()
}
}, {
rootMargin: '300px',
})
observer.observe(ref.current)
} else {
fillList(list, builder, { limit, ac })
}
})
if (limit && placeholder && fallback) {
const placeholders = Array
.from({ length: limit })
.map((_, index) => (
<Suspense
loader={placeholder({ ref: index === 0 ? ref : undefined })}
fallback={fallback}
signal={ac.signal}
>
{updateFromList(list, index)}
</Suspense>
))
return <>{placeholders}</>
}
return <>{list}</>
}
function fillList<ApiResponse>(
list: Signal<JSX.Element[]>,
builder: Builder<ApiResponse>,
{ limit, ac }: { limit?: number; ac?: AbortController },
) {
;(async () => {
const propsList = requestApiStream<void, ApiResponse>(
'news/fetchAll',
'GET',
)
for await (const props of propsList) {
list.value = [
...list.value,
builder(props),
]
if (limit && list.value.length >= limit) break
}
ac?.abort()
})()
}
function updateFromList(
list: Signal<JSX.Element[]>,
index: number,
): Promise<JSX.Element> {
const { promise, resolve } = Promise.withResolvers<JSX.Element>()
list.subscribe((value: JSX.Element[]) => {
const selected = value.at(index)
if (selected) {
resolve(selected)
}
})
return promise
}

View file

@ -16,7 +16,7 @@ function RenderError(
)
}
type Fallback = ({ error }: { error: Error }) => JSX.Element
export type Fallback = ({ error }: { error: Error }) => JSX.Element
export default function Suspense(
{ loader, fallback, signal, children }: {

View file

@ -19,7 +19,7 @@ export default function Home() {
<h2>Nos actus</h2>
<AutoGrid columnWidth='15rem' style={{ alignItems: 'center' }}>
<>
<BlogCardList limit={4} usePlaceholder={true} />
<BlogCardList limit={4} usePlaceholder={true} useObserver={true} />
<a href='/blog' class='cta'>Voir plus</a>
</>
</AutoGrid>