Get Started
Introduction
Components
Accordion
Badge
Button
Card
Checkbox
Command
Dialog
Dropdown Menu
Input
Select
Switch
Table
Tabs
Toast
Tooltip
Forms
Introduction
Validation
Field Arrays

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

8 articles
0 liked
0 saved
Page 1 / 5
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.