Infinite Scroll — Overview
A performance-optimized pattern for loading data as the user scrolls.
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.
<?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>