β¨ Flexible
β° Infinite Loop Detection
π£ Control Loader State
π IntersectionObserver based
π₯ Using Runes and Snippets
π§βπ§ Demo: svelte-5-infinite.vercel.app
[!WARNING]
v0.5.0contains a breaking change. See this PR for more details including migration steps.
svelte-infinitenpm install svelte-infinite
pnpm install svelte-infinite
yarn add svelte-infinite
InfiniteLoader and LoaderState from svelte-infinite<script lang="ts">
import { InfiniteLoader, LoaderState } from "svelte-infinite"
const loaderState = new LoaderState()
const allItems = $state([])
const loadMore = async () => {
const res = fetch("...")
const data = await res.json()
allItems.push(...data)
loaderState.loaded()
}
</script>
<InfiniteLoader {loaderState} triggerLoad={loadMore}>
{#each allItems as user (user.id)}
<div>{user.name}</div>
{/each}
</InfiniteLoader>
This is a more realistic example use-case which includes a paginated data endpoint that your triggerLoad function should hit every time it's called to load more data. It also includes the use of some of the optional snippets to render custom markup inside the loader component.
<script lang="ts">
// +page.svelte
import { InfiniteLoader, LoaderState } from "svelte-infinite"
import UserCard from "$components/UserCard.svelte"
const loaderState = new LoaderState()
const LOAD_LIMIT = 20
// Assume `$page.data.items` is the `+page.server.ts` server-side loaded
// and rendered initial 20 items of the list
const allItems = $state<{ id: number, body: string }[]>($page.data.items)
let pageNumber = $state(1)
// 1. This `loadMore` function is what we'll pass the InfiniteLoader component
// to its `triggerLoad` prop.
const loadMore = async () => {
try {
pageNumber += 1
const limit = String(LOAD_LIMIT)
const skip = String(LOAD_LIMIT * (pageNumber - 1))
// If there are less results on the first page (page.server loaded data)
// than the limit, don't keep trying to fetch more. We're done.
if (allItems.length < LOAD_LIMIT) {
loaderState.complete() // <--- using loaderState
return
}
const searchParams = new URLSearchParams({ limit, skip })
// Fetch an endpoint that supports server-side pagination
const dataResponse = await fetch(`/api/data?${searchParams}`)
// Ideally, like most paginated endpoints, this should return the data
// you've requested for your page, as well as the total amount of data
// available to page through
if (!dataResponse.ok) {
loaderState.error() // <--- using loaderState
// On errors, set the pageNumber back so we can retry
// that page's data on the next 'loadMore' attempt
pageNumber -= 1
return
}
const data = await dataResponse.json()
// If we've successfully received data, push it to the reactive state variable
if (data.items.length) {
allItems.push(...data.items)
}
// If there are more (or equal) number of items loaded as are totally available
// from the API, don't keep trying to fetch more. We're done.
if (allItems.length >= data.totalCount) {
loaderState.complete() // <--- using loaderState
} else {
loaderState.loaded() // <--- using loaderState
}
} catch (error) {
console.error(error)
loaderState.error() // <--- using loaderState
pageNumber -= 1
}
}
</script>
<main class="container">
<!-- 2. Here you wrap your items with the InfiniteLoader component -->
<InfiniteLoader {loaderState} triggerLoad={loadMore}>
{#each allItems as user (user.id)}
<UserCard {user} />
{/each}
<!-- 3. There are a few optional snippets for customizing what is shown at the bottom
of the scroller in various states, see the 'Snippets' section for more details -->
{#snippet loading()}
Loading...
{/snippet}
{#snippet error(load)}
<div>Error fetching data</div>
<button onclick={load}>Retry</button>
{/snippet}
</InfiniteLoader>
</main>
This package consists of two parts, first the InfiniteLoader component which is a wrapper around your items. It will trigger whichever async function you've passed to the triggerLoad prop when the user scrolls to the bottom of the list.
Second, there is also a LoaderState class which you should use to interact with the internal state of the loader. For example, if your fetch call errored, or you've reached the maximum number of items, etc. you can communicate that to the loader. The most basic usage example can be seen in the 'Getting Started' section above. A more complex example can be seen in the 'Example' section, and of course the application in /src/routes/+page.svelte in this repository also has a "real world" usage example.
[!WARNING] As of
0.5.0theLoaderStateimport is not an instance of the class, but the class itself. Meaning you'll need to instantiate it yourself withnew LoaderState()per component instance. This gives the user more flexibility when trying to use multiplesvelte-infiniteinstances per page, as well as resetting the state.
loaderState ControllerThe loaderState controller has 4 methods on it. You should call these at the appropriate times to control the internal state of the InfiniteLoader.
loaderState.loaded()READY so another fetch can be attempted.loaderState.error()InfiniteLoader to render a "Retry" button by default, or the error snippet.loaderState.complete()noData snippet.loaderState.reset()InfiniteLoader to its initial state, for example if there is a search input tied to your data and the user enters a new query.InfiniteLoader PropsloaderState: LoaderStateLoaderState class.triggerLoad: () => Promise<void> - requiredintersectionOptions: IntersectionObserverInit = { rootMargin: "0px 0px 200px 0px" } - optionalIntersectionObserver instance. See MDN for more details. The default rootMargin value will cause the target to intersect 200px earlier and trigger the loadMore function before it actually intersects with the root element (window by default). This has the effect of beginning to load the next page of data before the user has actually reached the current bottom of the list, making the experience feel more smooth.overflow-y: scroll) other than the window / viewport, then it might be necessary for you to also pass a custom root element here.loopTimeout: number = 3000 - optionalloopDetectionTimeout: number = 2000 - optionalloopMaxCalls count must be hit in order to trigger a cool down period.loopMaxCalls: number = 5 - optionaltriggerLoad executions which will trigger a cool down period, if reached within the loopDetectionTimeout.InfiniteLoader SnippetsSnippets replace slots in Svelte 5, and as such are used here to customize the content shown at the bottom of the scroller in various states. The InfiniteLoader component has 5 snippet "slots" available.
loadingtriggerLoad and waiting on a response.noResultsnoDataloaderState.complete() is called, indicating we've fetched and displayed all available data.coolingOffloaderState !== "COMPLETE" and a loop has been detected. Will disappear and loopTimeout when the cooling off period expires.errorloaderState.error() has been called. The snippet has an attemptLoad parameter passed to it which is just the internal triggerLoad function, designed for a "Retry" button or similar.MIT