Back to articles

Laravel Livewire Infinite Scrolling

March 15th, 2021

When I set out to implement infinite scrolling in Livewire, I didn't think it would be this simple. It turns out that loading more records with either the click of a button or the scroll of a browser window is incredibly straightforward.

Prefer watching? There's a dedicated course covering Livewire Infinite Scrolling over on Codecourse.

This isn't the most performant solution, as we'll see later. But it works nicely as long as you don't have a ridiculous number of overall results to show.

In this article, I'll guide you through coupling a simple Livewire component with Alpine.js to perform infinite loading of results of an article list.

Preface

I'm starting with a fresh Laravel installation and Livewire installed. I've also pulled in Laravel Breeze for the scaffolding. That's pretty much all we'll need here.

Article model and seeding

Feel free to skip this if you've already got a database full of items to display.

Create an Article model with a migration and factory

php artisan make:model Article -m -f

Keep it simple on the up migration with a title and a teaser.

public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->string('teaser');
        $table->timestamps();
    });
}

And of course, migrate.

php artisan migrate

Fill in the ArticleFactory to generate some fake data.

public function definition()
{
    return [
        'title' => $this->faker->sentence(5),
        'teaser' => $this->faker->sentence(20),
    ];
}

Using php artisan tinker or Tinkerwell if you have it, seed the database with 100 or more records.

Article::factory()->times(100)->create();

Displaying results with Livewire

Create a component to list through the articles. We'll call this... ArticleList.

php artisan livewire:make ArticleList

Open the ArticleList Livewire component, grab a collection of paginated articles (technically a LengthAwarePaginator) and pass them down to the corresponding Livewire Blade template.

class ArticleList extends Component
{
    public $perPage = 10;

    public function render()
    {
        $articles = Article::paginate($this->perPage);

        return view('livewire.article-list', [
            'articles' => $articles
        ]);
    }
}

Now in the view, iterate!

<div>
    @foreach ($articles as $article)
        <div class="mb-6">
            <h1 class="text-xl">#{{ $article->id }} {{ $article->title }}</h1>
            <p>{{ $article->teaser }}</p>
        </div>
    @endforeach
</div>

Add the article list to a page (in my case it's dashboard.blade.php) and check out the result.

<livewire:article-list />

You should see 10 articles listed on the page.

Loading more

This is where Livewire shines.

First, create a method in the ArticleList component that increments the $perPage property by your chosen increment.

class ArticleList extends Component
{
    public $perPage = 10;

    public function loadMore()
    {
        $this->perPage += 10;
    }

    // ...
}

Now add a button below the list of articles to invoke this method.

<div>
    @foreach ($articles as $article)
        <div class="mb-6">
            <h1 class="text-xl">#{{ $article->id }} {{ $article->title }}</h1>
            <p>{{ $article->teaser }}</p>
        </div>
    @endforeach
    
    @if($articles->hasMorePages())
        <button wire:click.prevent="loadMore">Load more</button>
    @endif
</div>

Notice we're adding a check using hasMorePages from Laravel's LengthAwarePaginator. We don't need to show the button if there are no more results to load.

Now click the Load more button. By incrementing the $perPage property, we're gradually showing more results.

A look behind the scenes

Just before we use the JavaScript Intersection Observer API to add infinite scroll behaviour, it's important to look at what's happening behind the scenes here, and why it's not the most performant solution.

This is an incredibly easy way of implementing this kind of behaviour, but has a slight performance issue, depending on how many overall results you have.

Every time we do Article::paginate($this->perPage), we're requesting the new, incremented amount of results to show in the list.

  1. On first page load, we load 10 results.
  2. When we hit Load more, we load in 20 results overall.
  3. When we hit it again and again, we're loading 30, 40, 50 and so on.

This means that each press of Load more queries the database for the current number of results we want to display per page, not the next set of 10.

It's fine for now, and we'll cover a different approach in another article!

Bringing in the Intersection Observer API

For infinite scroll, we'll now trigger the loadMore method when the user reaches the bottom of the current list of displayed articles.

Because I used Laravel Breeze for this article, I already have Alpine.js pulled in. You're welcome to implement this behaviour without Alpine, but if you'd like to give it a try, head over to the Alpine.js documentation and pull it into your project.

Ready? Just below the @endforeach, add the following Alpine component.

<div
    x-data="{
        observe () {
            let observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        @this.call('loadMore')
                    }
                })
            }, {
                root: null
            })

            observer.observe(this.$el)
        }
    }"
    x-init="observe"
></div>

What's going on here?

On initialisation, we're calling observe, which registers an IntersectionObserver for the current element (this div, referenced by this.$el in Alpine).

By iterating over the entries, which represent the intersection between the target element and the root container, we'll check if we have an intersection (in short – is this div visible?).

If it's visible, we use @this, which represents the current Livewire component (ArticleList) by its unique ID to call the loadMore method.

That's pretty much it. Once that Alpine component is in there, scrolling to the bottom of the page will trigger the loading of more results as we've previously seen from clicking the Load more button. It's probably a good idea to leave the Load more button in there, just in-case the Intersection Observer API isn't supported for the user.

I'd recommend pulling the Intersection Observer polyfill into your project anyway, as there are still some browsers without support for it (but, at least you have that Load more button!).

Author
Alex Garrett-Smith

Comments

No coments, yet. Be the first to leave a comment.