feat: load post cursors infinitely

This commit is contained in:
2026-07-13 21:43:44 +09:00
parent 80a1f66b26
commit aac376a571
7 changed files with 772 additions and 0 deletions
@@ -0,0 +1,93 @@
import { useEffect, useRef } from 'react'
import { flattenPostPages } from '../page'
import { type FeedRequest, PostLoadError, usePostFeed } from '../use-post-feed'
import { PostCard } from './post-card'
export function PostFeed({ request }: { request: FeedRequest | undefined }) {
const query = usePostFeed(request)
const sentinel = useRef<HTMLDivElement>(null)
const fetchingNextPage = useRef(query.isFetchingNextPage)
const posts = query.data ? flattenPostPages(query.data.pages) : []
fetchingNextPage.current = query.isFetchingNextPage
useEffect(() => {
if (
!sentinel.current ||
!query.hasNextPage ||
query.isFetchNextPageError ||
query.isFetchingNextPage
) {
return
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting && !fetchingNextPage.current) {
fetchingNextPage.current = true
void query.fetchNextPage()
}
},
{ rootMargin: '600px 0px' },
)
observer.observe(sentinel.current)
return () => observer.disconnect()
}, [
query.fetchNextPage,
query.hasNextPage,
query.isFetchNextPageError,
query.isFetchingNextPage,
])
if (!request) return null
if (query.isPending) {
return (
<p className="state" role="status">
投稿を取得しています…
</p>
)
}
if (query.isError && !query.isFetchNextPageError && posts.length === 0) {
const error =
query.error instanceof PostLoadError
? query.error.detail
: { message: '投稿を取得できませんでした。', retryable: true }
return (
<div className="state" role="alert">
<p>{error.message}</p>
{error.retryable ? (
<button onClick={() => void query.refetch()} type="button">
再試行
</button>
) : null}
</div>
)
}
if (posts.length === 0 && !query.hasNextPage) {
return <p className="state">条件に一致する投稿はありません。</p>
}
return (
<section aria-live="polite" className="feed">
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
<div aria-hidden="true" ref={sentinel} />
{query.isFetchingNextPage ? (
<div className="loading-rail" role="status">
次の投稿を取得しています…
</div>
) : null}
{query.isFetchNextPageError ? (
<div className="state" role="alert">
<p>続きの投稿を取得できませんでした。</p>
<button onClick={() => void query.fetchNextPage()} type="button">
再試行
</button>
</div>
) : null}
{!query.hasNextPage ? (
<p className="state">これ以上の投稿はありません。</p>
) : null}
</section>
)
}