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.
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.
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();
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.
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.
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.
Load more
, we load in 20 results overall.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!
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!).