Infinite Scroll — Overview

A performance-optimized pattern for loading data as the user scrolls.

View Live Demo

This example demonstrates a basic infinite scrolling pattern using an IntersectionObserver and a sentinel element at the end of the list.

pp.fetchFunction is provided by the Prisma PHP Core utilities (not PulsePoint). PulsePoint inherits those utilities and adds friendly conventions — you can use PulsePoint's conventions out-of-the-box or supply your own. The improved pp.fetchFunction aims to provide a Next.js-like server-function DX: call backend PHP functions directly from the client with consistent serialization, abort support, retries, and structured error handling.

Key improvements:

  • Call named backend functions (by name) and receive typed JSON responses.
  • Supports options: method, headers, timeout, retry, and AbortController via signal.
  • Built-in error shape and optional onError callback for centralized handling.
  • Pluggable serialization so you can use your own conventions if needed.

Example usage (client-side):

// Call server function 'getFeed' like a Next.js server function
const controller = new AbortController();

const opts = {
  method: 'POST',           // optional: POST/GET
  headers: { 'X-Client': 'pulsepoint' },
  timeout: 10000,           // ms
  retry: 1,                 // retry once on transient failures
  signal: controller.signal,
  // Optional hook for centralized error handling
  onError: (err) => {
    console.error('fetchFunction error', err);
  }
};

const { response, error } = await pp.fetchFunction('getFeed', { page, size: pageSize }, opts);

if (error) {
  // handle structured error: { message, code, details }
  console.error(error);
} else {
  setItems([...items, ...response.items]);
}

// If user navigates away or you need to cancel:
controller.abort();

Note (server-side): expose your PHP function through the framework bootstrap so pp.fetchFunction can call it by name (example registration varies by app). The mechanism is pluggable — PulsePoint conventions are provided but optional.

infinite-scroll.html
<?php

function getFeed($data)
{
    $page = $data->page ?? 1;
    $size = $data->size ?? 50;

    $items = [];
    for ($i = 0; $i < $size; $i++) {
        $id = ($page - 1) * $size + $i + 1;
        $items[] = [
            'id' => $id,
            'title' => "Item $id",
            'snippet' => "This is a snippet for item $id."
        ];
    }
    return ['items' => $items];
}

?>

<h1>🔄 Infinite Scroll</h1>

<p>Current page: {page}</p>

<!-- Container with overflow for scrolling -->
<div id="feed" style="height: 80vh; overflow: auto">
    
    <!-- Loading Indicator -->
    <div hidden="{!firstLoad}">
        <p>Loading…</p>
    </div>

    <!-- List Items -->
    <template pp-for="item in items">
        <article key="{item.id}">
            <h3>{item.title}</h3>
            <p>{item.snippet}</p>
        </article>
    </template>

    <!-- Sentinel Element (Trigger for next page) -->
    <div id="sentinel" style="height: 1px"></div>
</div>

<script>
    const [items, setItems] = pp.state([]);
    const [page, setPage] = pp.state(1);
    const [firstLoad, setFirstLoad] = pp.state(true);

    const pageSize = 50;
    let loading = false,
        done = false;

    async function loadMore() {
        if (loading || done) return;
        loading = true;
        
        try {
            // Fetch Data pp.fetchFunction its Prisma PHP Core utility for server calls
            const { response } = await pp.fetchFunction('getFeed', {
                page,
                size: pageSize
            });

            setItems([...items, ...response.items]);
            setPage(page + 1);

            if (firstLoad) setFirstLoad(false);

            // Stop if no more items returned
            if (!response.items?.length || response.items.length < pageSize) {
                done = true;
            }
        } finally {
            loading = false;
        }
    }

    // Setup Observer
    pp.effect(() => {
        const root = document.getElementById('feed');
        const sentinel = root.querySelector('#sentinel');
        
        const io = new IntersectionObserver(
            (entries) => {
                if (entries.some((e) => e.isIntersecting)) loadMore();
            }, 
            {
                root,
                rootMargin: '200px 0px' // Load 200px before reaching bottom
            }
        );

        io.observe(sentinel);
        return () => io.disconnect();
    }, []);
</script>
Todo List Next: Paginate