Async Infinite Scroll
IntersectionObserver-triggered pagination with <Async> streaming boundary, mapArray append, per-item like/save actions, and effect cleanup on unmount. Tests the IRAsync + mapArray compiler path, reactive list growth, and error/empty-state branches.
#Preview
1"use client"23import { createSignal, createMemo, onMount, onCleanup } from '@barefootjs/client'45type Article = { id: number; title: string; liked: boolean; saved: boolean; /* ... */ }6type FetchStatus = 'idle' | 'loading' | 'error' | 'end'78export function InfiniteScrollDemo() {9 const [items, setItems] = createSignal<Article[]>(INITIAL_ITEMS)10 const [cursor, setCursor] = createSignal(1)11 const [status, setStatus] = createSignal<FetchStatus>('idle')1213 const totalCount = createMemo(() => items().length)14 const likedCount = createMemo(() => items().filter(a => a.liked).length)1516 const toggleLike = (id: number) => {17 setItems(prev => prev.map(a => a.id === id ? { ...a, liked: !a.liked } : a))18 }1920 const loadMore = async () => {21 if (status() === 'loading' || status() === 'end') return22 setStatus('loading')23 try {24 const newItems = await fetchPage(cursor())25 setItems(prev => [...prev, ...newItems]) // mapArray append26 setCursor(c => c + 1)27 setStatus('idle')28 } catch {29 setStatus('error')30 }31 }3233 onMount(() => {34 const sentinel = document.querySelector('.is-sentinel')35 const observer = new IntersectionObserver(36 entries => { if (entries[0].isIntersecting) loadMore() },37 { threshold: 0.1 }38 )39 observer.observe(sentinel!)40 onCleanup(() => observer.disconnect()) // effect cleanup on unmount41 })4243 return (44 <div>45 <div>{totalCount()} articles · {likedCount()} liked</div>4647 {/* <Async> boundary — IRAsync wrapping a signal-driven map */}48 <Async fallback={<ArticleSkeleton />}>49 <div data-slot="article-list">50 {items().map(article => (51 <article key={article.id}>52 <h3>{article.title}</h3>53 <button54 aria-pressed={article.liked ? 'true' : 'false'}55 onClick={() => toggleLike(article.id)}56 >57 {article.liked ? '♥' : '♡'}58 </button>59 </article>60 ))}61 </div>62 </Async>6364 {/* Sentinel + status branches */}65 <div className="is-sentinel">66 {status() === 'loading' ? <p>Loading…</p> : null}67 {status() === 'error' ? <button onClick={loadMore}>Retry</button> : null}68 {status() === 'end' ? <p>You have reached the end · {totalCount()} articles</p> : null}69 </div>70 </div>71 )72}#Features
<Async> Boundary + mapArray
The initial article list is wrapped in an <Async fallback={skeleton}> boundary. In SSR the compiler emits a <Suspense> node (IRAsync → Hono adapter) containing the items().map() loop. This exercises the IRAsync + IRMap compiler path that was previously untested by any block demo.
IntersectionObserver + Effect Cleanup
onMount registers an IntersectionObserver on the sentinel div at the bottom of the list. onCleanup(() => observer.disconnect()) ensures the observer is torn down if the component unmounts mid-fetch, preventing stale callbacks and memory leaks.
mapArray Append
Each page load calls setItems(prev => [...prev, ...newItems]), appending to the existing signal array. BarefootJS's client-side mapArray reconciles the new items by keyed diffing — only the new DOM nodes are created; existing article cards are not re-rendered.
Error and Empty States
A createSignal<FetchStatus> drives three conditional branches: loading (spinner), error (retry button), and end (end-of-list message). The 12% simulated error rate makes the retry branch reachable during testing.
Per-Item Reactive Actions
Each article card has like and save toggles that call setItems(prev => prev.map(a => a.id === id ? ...)), an immutable update inside a reactive loop. createMemo chains derive aggregate counts (liked, saved) from the items signal, updating the stats bar reactively.