DEV Community

Bilal Haidar
Bilal Haidar

Posted on

Implementing Infinite Scrolling with Laravel, Inertia.js v2.0, and Vue 3

In this comprehensive guide, we'll explore how to implement infinite scrolling in a Laravel application using Inertia.js v2.0 and Vue 3. We'll cover both the frontend and backend implementation, with special attention to handling full page refreshes and maintaining scroll position.

Table of Contents

Understanding the Components

The infinite scrolling implementation relies on three main components:

  1. Inertia.js v2.0's WhenVisible Component: This component handles the intersection observer logic to detect when we need to load more content.
  2. Laravel's Pagination: Handles the server-side pagination logic.
  3. Vue 3's Composition API: Manages our frontend state and reactivity.

Frontend Implementation

Let's start with a Vue component that implements infinite scrolling for a blog posts listing:

<script setup>
import { computed } from 'vue'
import { usePage, WhenVisible } from '@inertiajs/vue3'
import LoadingSpinner from '@/components/LoadingSpinner.vue'
import BlogPostCard from '@/components/BlogPostCard.vue'

const page = usePage()

const hasFeaturePost = computed(() => !!page.props.featuredPost)
const categoryName = computed(() => page.props.category?.name)
</script>

<template>
    <div class="bg-gray-50">
        <!-- Featured Post Section -->
        <div
            v-if="hasFeaturePost"
            class="container"
        >
            <div class="py-8 text-center">
                <h2 class="mb-4 text-3xl font-bold">
                    Featured Post: {{ page.props.featuredPost.title }}
                </h2>
            </div>
        </div>

        <!-- Posts Grid -->
        <div class="max-w-7xl px-4 py-8 mx-auto sm:px-6 lg:px-8">
            <h1 class="text-2xl font-bold mb-6">
                {{ categoryName ? `Posts in ${categoryName}` : 'All Posts' }}
            </h1>

            <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
                <div
                    v-for="post in page.props.posts"
                    :key="post.id"
                    class="relative flex flex-col"
                >
                    <blog-post-card :post="post" />
                </div>

                <!-- Infinite Scroll Handler -->
                <WhenVisible
                    :always="page.props.postsPagination?.current_page < page.props.postsPagination?.last_page"
                    :params="{
                        data: {
                            page: page.props.postsPagination.current_page + 1,
                        },
                        only: ['posts', 'postsPagination'],
                    }"
                >
                    <div
                        v-if="page.props.postsPagination?.current_page >= page.props.postsPagination?.last_page"
                        class="text-center py-6 text-gray-600 col-span-1 md:col-span-2 lg:col-span-3"
                    >
                        You've reached the end!
                    </div>
                    <div
                        v-else
                        class="col-span-1 md:col-span-2 lg:col-span-3"
                    >
                        <loading-spinner />
                    </div>
                </WhenVisible>
            </div>
        </div>
    </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Key Frontend Features

  1. WhenVisible Component: This component from Inertia.js v2.0 automatically triggers a request when the element becomes visible in the viewport.

  2. Pagination Parameters:

:params="{
    data: {
        page: page.props.postsPagination.current_page + 1,
    },
    only: ['posts', 'postsPagination'],
}"
Enter fullscreen mode Exit fullscreen mode
  • data: Specifies the next page to load
  • only: Optimizes the request by only fetching required data
  1. Loading States: The component handles both loading and end-of-content states elegantly.

Backend Implementation

Here's the Laravel controller implementation that handles both regular pagination and full-page load scenarios:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use App\Models\Category;
use Illuminate\Pagination\LengthAwarePaginator;
use Inertia\Inertia;

class BlogController extends Controller
{
    public function index(?Category $category = null)
    {
        return Inertia::render('Blog/Index', [
            'category' => $category,
            'featuredPost' => $this->getFeaturedPost(),
            'posts' => $this->getPaginatedPosts($category),
            'postsPagination' => $this->getPaginatedPosts($category)?->toArray(),
        ]);
    }

    protected function getPaginatedPosts(?Category $category): ?LengthAwarePaginator
    {
        $currentPage = request()->input('page', 1);
        $perPage = request()->input('per_page', 12);

        $query = Post::query()
            ->with(['author', 'category'])
            ->published();

        if ($category) {
            $query->where('category_id', $category->id);
        }

        // Apply any additional filters
        if (request()->has('sort')) {
            $query->orderBy(request()->input('sort'), request()->input('direction', 'desc'));
        } else {
            $query->latest();
        }

        // Handle full page load vs. infinite scroll request
        if (!request()->header('X-Inertia')) {
            // Full page load - fetch all pages up to current
            $allResults = collect();

            for ($page = 1; $page <= $currentPage; $page++) {
                $pageResults = $query->paginate($perPage, ['*'], 'page', $page);
                $allResults = $allResults->concat($pageResults->items());
            }

            return new LengthAwarePaginator(
                $allResults,
                Post::query()
                    ->published()
                    ->when($category, fn($q) => $q->where('category_id', $category->id))
                    ->count(),
                $perPage,
                $currentPage
            );
        }

        return $query->paginate($perPage);
    }

    protected function getFeaturedPost()
    {
        return Post::query()
            ->with(['author', 'category'])
            ->published()
            ->featured()
            ->latest()
            ->first();
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Backend Features

  1. Pagination Handling:
if (!request()->header('X-Inertia')) {
    // Full page load logic
} else {
    // Regular pagination for infinite scroll
}
Enter fullscreen mode Exit fullscreen mode
  1. Full Page Load: When a user refreshes or directly visits a page, we fetch all previous pages to maintain the correct scroll position:
for ($page = 1; $page <= $currentPage; $page++) {
    $pageResults = $query->paginate($perPage, ['*'], 'page', $page);
    $allResults = $allResults->concat($pageResults->items());
}
Enter fullscreen mode Exit fullscreen mode
  1. Efficient Querying: The implementation includes relationship eager loading and scoped queries:
$query = Post::query()
    ->with(['author', 'category'])
    ->published();
Enter fullscreen mode Exit fullscreen mode

Conclusion

Implementing infinite scrolling with Laravel and Inertia.js v2.0 provides a smooth user experience while maintaining good performance and SEO practices. The combination of Vue 3's Composition API and Inertia.js's WhenVisible component makes it easy to implement and maintain.

Remember to:

  • Test the implementation thoroughly, especially for edge cases
  • Monitor performance metrics
  • Consider implementing a fallback for users with JavaScript disabled
  • Keep accessibility in mind when implementing infinite scroll

This implementation can be adapted for various use cases beyond blog posts, such as product listings, image galleries, or any other content that benefits from infinite scrolling.

Additional Resources

Top comments (0)