Skip to content

ScrollStack JsInfinite scrolling, without the markup

Three options in, a snapshot out. It handles paging, retries, cancellation and the scroll trigger — you write the HTML.

ScrollStack

Install

bash
npm i @scrollstackjs/react     # or /vue, /svelte, or /core on its own

Copy this

A real, working feed. The API is free and needs no key, so you can paste this and run it right now.

tsx
import { useInfiniteScroll } from '@scrollstackjs/react'

function Feed() {
  const { pages, ref, hasNextPage, isFetchingNextPage } = useInfiniteScroll({
    initialPageParam: 'https://rickandmortyapi.com/api/character?page=1',
    fetchPage: async ({ pageParam, signal }) => (await fetch(pageParam, { signal })).json(),
    getNextPageParam: (last) => last.info.next, // null = no more pages
  })

  return (
    <ul>
      {pages
        .flatMap((p) => p.results)
        .map((c) => (
          <li key={c.id}>{c.name}</li>
        ))}

      {/* Scroll this into view and the next page loads. */}
      {hasNextPage && <li ref={ref}>{isFetchingNextPage ? 'Loading…' : ''}</li>}
    </ul>
  )
}
vue
<script setup lang="ts">
import { useInfiniteScroll } from '@scrollstackjs/vue'
import { computed } from 'vue'

const { state, target } = useInfiniteScroll({
  initialPageParam: 'https://rickandmortyapi.com/api/character?page=1',
  fetchPage: async ({ pageParam, signal }) => (await fetch(pageParam, { signal })).json(),
  getNextPageParam: (last) => last.info.next, // null = no more pages
})

const characters = computed(() => state.value.pages.flatMap((p) => p.results))
</script>

<template>
  <ul>
    <li v-for="c in characters" :key="c.id">{{ c.name }}</li>

    <!-- Scroll this into view and the next page loads. -->
    <li v-if="state.hasNextPage" :ref="target">
      {{ state.isFetchingNextPage ? 'Loading…' : '' }}
    </li>
  </ul>
</template>
svelte
<script lang="ts">
  import { onDestroy } from 'svelte'
  import { createInfiniteScroll } from '@scrollstackjs/svelte'

  const scroll = createInfiniteScroll({
    initialPageParam: 'https://rickandmortyapi.com/api/character?page=1',
    fetchPage: async ({ pageParam, signal }) => (await fetch(pageParam, { signal })).json(),
    getNextPageParam: (last) => last.info.next, // null = no more pages
  })
  const { target } = scroll
  onDestroy(scroll.destroy)
</script>

<ul>
  {#each $scroll.pages.flatMap((p) => p.results) as c (c.id)}
    <li>{c.name}</li>
  {/each}

  <!-- Scroll this into view and the next page loads. -->
  {#if $scroll.hasNextPage}
    <li use:target>{$scroll.isFetchingNextPage ? 'Loading…' : ''}</li>
  {/if}
</ul>
ts
import { createInfiniteScroll } from '@scrollstackjs/core'

const scroll = createInfiniteScroll({
  initialPageParam: 'https://rickandmortyapi.com/api/character?page=1',
  fetchPage: async ({ pageParam, signal }) => (await fetch(pageParam, { signal })).json(),
  getNextPageParam: (last) => last.info.next,
})

scroll.subscribe(() => render(scroll.getSnapshot()))
scroll.observeTarget(sentinelElement)

The three options

That is the whole required API:

OptionWhat it is
initialPageParamWhat to fetch first — a URL, a cursor, 0, whatever your API wants.
fetchPageHow to fetch one page. Gets the param and an AbortSignal.
getNextPageParamWhere the next page is. Return null when there are no more.

Everything else — pages, hasNextPage, isLoading, retries, cancellation — you get back for free.

Where to next

  • Get started — install, the full snapshot, and the one rule about sentinels.
  • Examples — nine running demos, each with the code in all four flavors: paging, retries, virtual lists, devtools and more.
  • Live demo — play with the behavior first, read later.