Infinite Scroll#

Load more items as the user scrolls.

See also

Official Inertia.js docs: Infinite Scroll

What Is Infinite Scroll?#

Infinite scroll combines partial reloads with merge props so new data is appended to the existing list rather than replacing it.

Backend Usage#

Mark list props as mergeable and provide scroll metadata:

from litestar import get
from litestar.params import FromQuery
from litestar_vite.inertia import merge, scroll_props, InertiaResponse

@get("/posts", component="Posts")
async def list_posts(page: FromQuery[int] = 1) -> InertiaResponse:
    posts = await Post.paginate(page=page, per_page=20)
    return InertiaResponse(
        {"posts": merge("posts", posts.items, match_on="id")},
        scroll_props=scroll_props(
            page_name="page",
            current_page=page,
            previous_page=page - 1 if page > 1 else None,
            next_page=page + 1 if posts.has_next else None,
        ),
    )

Automatic Scroll Props#

You can also return a pagination container and enable infinite_scroll on the route. Litestar-Vite will extract items and scroll props for you:

from litestar.params import FromQuery
from litestar.pagination import OffsetPagination

@get("/posts", component="Posts", infinite_scroll=True, key="posts")
async def list_posts(offset: FromQuery[int] = 0, limit: FromQuery[int] = 20) -> OffsetPagination[Post]:
    posts, total = await Post.paginate(offset=offset, limit=limit)
    return OffsetPagination(items=posts, total=total, limit=limit, offset=offset)

Frontend Usage#

Trigger partial reloads when the bottom sentinel becomes visible:

import { usePage, WhenVisible } from "@inertiajs/react";

export default function Posts() {
  const page = usePage();
  const { posts } = page.props;
  const postScroll = page.props.scrollProps?.posts;

  return (
    <div>
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}

      {postScroll?.nextPage && (
        <WhenVisible
          always
          params={{ only: ["posts"], data: { page: postScroll.nextPage } }}
        >
          <Spinner />
        </WhenVisible>
      )}
    </div>
  );
}
<script setup lang="ts">
import { usePage, WhenVisible } from "@inertiajs/vue3";

const page = usePage();
const { posts } = page.props;
</script>

<template>
  <div>
    <PostCard v-for="post in posts" :key="post.id" :post="post" />

    <WhenVisible
      v-if="page.props.scrollProps?.posts?.nextPage"
      always
      :params="{ only: ['posts'], data: { page: page.props.scrollProps.posts.nextPage } }"
    >
      <Spinner />
    </WhenVisible>
  </div>
</template>

Tips#

  • Use match_on in merge() to avoid duplicates when refreshing.

  • Use page_name to isolate multiple scroll regions on one page.

  • Combine with Remembering State to preserve filters across visits.

See Also#