Infinite Scroll with Inertia V2 using ReactJS

Prerequisite

Installed Laravel 11 project with React Inertia V2. If you don’t have, then you need to install it

laravel new infinite-scroll-app # Install laravel app

Would you like to install a starter kit? [No starter kit]:
 [none     ] No starter kit
 [breeze   ] Laravel Breeze
 [jetstream] Laravel Jetstream
> breeze


Which Breeze stack would you like to install? [Blade with Alpine]:
 [blade              ] Blade with Alpine
 [livewire           ] Livewire (Volt Class API) with Alpine
 [livewire-functional] Livewire (Volt Functional API) with Alpine
 [react              ] React with Inertia
 [vue                ] Vue with Inertia
 [api                ] API only
> react


Would you like any optional features? [None]:
 [none      ] None
 [dark      ] Dark mode
 [ssr       ] Inertia SSR
 [typescript] TypeScript
 [eslint    ] ESLint with Prettier
> dark,ssr,eslint # You can choose what you need here


Which testing framework do you prefer? [Pest]:
 [0] Pest
 [1] PHPUnit
> 0

# Process
# Installing Laravel....

Which database will your application use? [MySQL]:
 [mysql  ] MySQL
 [mariadb] MariaDB
 [pgsql  ] PostgreSQL
 [sqlite ] SQLite (Missing PDO extension)
 [sqlsrv ] SQL Server (Missing PDO extension)
> pgsql # I'll use postgresql here

Default database updated. Would you like to run the default database migrations? (yes/no) [yes]:
> no # Because we need to configure .env first

# Process
# Installing breeze..

cd infinite-scroll-app

Configure .env to connect into our Database

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5433 # You can use 5432 for default port of pgsql
DB_DATABASE=infinite_scroll_app
DB_USERNAME=postgres
DB_PASSWORD=postgres

Generate Placeholder Data

You can totally use any kind of data for "infinite scroll." For example, I'm going to use posts data here.

php artisan make:model Post -mf # This will create a model, migration, and factory in one go

This is migration file should look like

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

To avoid mass assignment problem we need to tell our model Post.php which field is $fillable or if all field is fillable except id, then we can use $guarded. You can learn the detail here

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /** @use HasFactory<\Database\Factories\PostFactory> */
    use HasFactory;

    protected $guarded = ['id'];
}

Next we need to create a dummy data. Laravel can easily make dummy data using factories. Open PostFactory.php that we already create it earlier using php artisan command, then we are using faker to generate random content (it already included in Laravel btw)

<?php

namespace Database\Factories;

use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
 */
class PostFactory extends Factory
{
    protected $model = Post::class; # We can specify our models here (optional)

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'title' => $this->faker->sentence(5),
            'slug' => $this->faker->slug(5),
            'content' => $this->faker->text(),
        ];
    }
}

Then generate 50 data using seeder, the seeder file is located in database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Post;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        Post::factory()->count(50)->create();
    }
}

Apply Migration

After writing our models and prepare it using placeholder data via factory, then we can apply the migrations and seeding Post data

php artisan migrate

The database 'infinite_scroll_app' does not exist on the 'pgsql' connection.
Would you like to create it? (yes/no) [yes]
❯ yes # Because i dont have the database yet, So i create it

php artisan db:seed # Run DatabaseSeeder.php

Run the App

Because we use Laravel breeze starter kit, we should have auth (login or register) and dashboard pages. You can run the app by running this command

php artisan ser

# Open your second terminal
npm run dev

Visit http://localhost:8000/register for registering new user account, fill the form, and you could be able to visit dashboard page like this

Dashboard Overview

Query Post data

On file routes/web.php edit /dashboard route callback to returning the Post data. Best practices are by using controller, it is just only testing purposes in this tutorial

Route::get('/dashboard', function (Request $request) {
    $perPage = 10; // You can also get it on $request->query()
    $page = $request->query('page', 1);

    $posts = Post::paginate($perPage); // Paginate to load lazy post data

    $postPaginateProp = $posts->toArray(); // To access paginate property
    $isNextPageExists = $postPaginateProp['current_page'] < $postPaginateProp['last_page'];

    // Passing to the frontend
    return Inertia::render('Dashboard', [
        'posts' => Inertia::merge($posts->items()),
        'page' => $page,
        'isNextPageExists' => $isNextPageExists
    ]);
})->middleware(['auth', 'verified'])->name('dashboard');

You notice we use Inertia::merge. This is useful if you need to merging latest data into previous props data. On first load we query just 10 Post data, then when are doing refetch in current route, so it will merge latest Post data (which is from 11 to 20) into our existing Post data before (1 to 10). Checkout the detail on Inertia merge documentation

Update Dashboard.jsx to show all post data coming from inertia props.

import { 
    Head, 
    WhenVisible // Import <WhenVisible/> Inertia built in components
} from '@inertiajs/react';

export default function Dashboard(props) {
    return (
        ...
               {props.posts.map((post, idx) => (
                    <div
                        key={idx}
                        className="p-6 text-gray-900 dark:text-gray-100"
                    >
                        {post.title}
                    </div>
                ))}
                {props.isNextPageExists && (
                    <WhenVisible
                        always
                        params={{
                            data: {
                                page: +props.page + 1,
                            },
                            only: ['posts', 'page', 'isNextPageExists'],
                        }}
                        fallback={<p>You reach the end.</p>}
                    >
                        <p>Loading...</p>
                    </WhenVisible>
                )}
            ...

On this code, we use <WhenVisible></WhenVisible> component with condition isNextPageExists (you should understand what I mean by just reading the variable name). This component will be rendered on page when there is next page on post pagination property that we already define before. It will trigger when this element become visible on the viewport using IntersectionObserverAPI. If there is next page of pagination, so it will make a request into current route (which is /dashboard) to get latest post data based on params object.

There is an option to define what props to be updated using only options in params prop. Here we define posts, page, and isNextPageExists props to be updated when triggered request is done.

By default, the WhenVisible component will only trigger once when the element becomes visible. If you want to always trigger the data when the element is visible, you can provide the always prop. Checkout this for more details

Demo GIF

And finally you can see it infinitely scrollable if there is another post data available. Thanks for reading 😍, any suggestion would be appreciated

FYI: I’m currently learning to writing in English for my IELTS. I try my best to write this article without any Copy and Paste generated text from an AI. If there is any wrong grammar, feel free to correct me. This Post is going to be my witness for my future self that I’m willing to learn and growth 😊

0
Subscribe to my newsletter

Read articles from Adi Cahya Saputra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Adi Cahya Saputra
Adi Cahya Saputra

Fullstack Developer | UI/UX Designer | Web & Mobile | NextJS, Laravel, ASP .NET Core, Kotlin, Flutter, React Native, Figma