Infinite Scrolling with Livewire v3 + AlpineJS without impacting performance
Victor Lima
Posted on February 22, 2024
Laravel Livewire developers may know that public Models in a component are automatically hydrated every request.
This means that everytime a request is sent against the server, under the hood Livewire "refreshes" the model, retrieving it again from the database.
But did you know that eager loaded models are also refreshed?
The Issue
Let's jump into the code. For this article, I have a simple users list, where each user has an eager loaded Role.
I wrote a simple infinite scrolling using limit
and offset
, and it loads more every time the user sees the end of the the current page (x-intersect
), until there are no data left to render.
In my Livewire component:
public const int PER_PAGE = 10;
public Collection $users;
public function mount(): void
{
$this->users = collect();
$this->loadUsers();
}
public function loadUsers(bool $loadingMore = false): void
{
User::query()
->with('role')
->limit(10)
->when(
$loadingMore,
fn (Builder $query) => $query->offset($this->users->count())
)
->get()
->each(fn (User $user) => $this->users->push($user));
}
public function loadMore(): void
{
$this->loadUsers(loadingMore: true);
}
#[Computed]
public function total(): int
{
return User::count();
}
#[Computed]
public function hasMore(): bool
{
return $this->users->count() < $this->total;
}
In the Livewire component's view:
@foreach ($this->users as $key => $user)
<li class="flex justify-between gap-x-6 py-5"
wire:key="user-{{ $key }}">
<div class="flex min-w-0 gap-x-4">
<img class="h-12 w-12 flex-none rounded-full bg-gray-50"
src="https://ui-avatars.com/api/?background=0D8ABC&color=fff&name={{ $user->name }}"
alt="">
<div class="min-w-0 flex-auto">
<p class="text-sm font-semibold leading-6 text-gray-900">{{ $user->name }}</p>
<p class="mt-1 truncate text-xs leading-5 text-gray-500">{{ $user->email }}</p>
</div>
</div>
<div class="hidden shrink-0 sm:flex sm:flex-col sm:items-end">
<h3 class="text-gray-800 font-bold text-sm">Role</h3>
<p class="text-sm leading-6 text-gray-900">{{ $user->role->name }}</p>
</div>
</li>
@endforeach
@if ($this->hasMore)
<div class="self-center loading loading-spinner" x-intersect="$wire.loadMore()"></div>
@endif
So, since I have 10 User
models per page with an eager loaded Role
, every time I go to the next page, Livewire automatically hydrates every existing public model and lazy load its relationships, making many additional queries per request, as seen in Debugbar:
Scrolled to the end of the list:
Note that for each page, it refreshes all the previous models, so it becomes a performance issue. Easy to fix, but an issue anyway.
Solution
So, how do we solve this without impacting performance and memory usage?
The answer: the good old arrays.
By parsing all models to array, Livewire won't hydrate each record when going to the next page or if the component hydrates by any other reason, because this behavior of course only occurs with public Eloquent Models.
So, let's dive into the solution:
First, let's write a method to parse our models to array considering the data we actually need to use in the frontend.
private function parseUser(User $user): array
{
$role = $user->role->only(['id', 'name']);
$user = $user->only([
'id',
'name',
'email'
]);
return array_merge(
$user,
['role' => $role]
);
}
Then, when adding the User
model to the users
Collection, we just parse it first:
->each(fn (User $user) => $this->users->push($this->parseUser($user)));
The only thing left is to adjust our view to use arrays instead of Models:
@foreach ($this->users as $key => $user)
<li class="flex justify-between gap-x-6 py-5"
wire:key="user-{{ $key }}">
<div class="flex min-w-0 gap-x-4">
<img class="h-12 w-12 flex-none rounded-full bg-gray-50"
src="https://ui-avatars.com/api/?background=0D8ABC&color=fff&name={{ $user['name'] }}"
alt="">
<div class="min-w-0 flex-auto">
<p class="text-sm font-semibold leading-6 text-gray-900">{{ $user['name'] }}</p>
<p class="mt-1 truncate text-xs leading-5 text-gray-500">{{ $user['email'] }}</p>
</div>
</div>
<div class="hidden shrink-0 sm:flex sm:flex-col sm:items-end">
<h3 class="text-gray-800 font-bold text-sm">Role</h3>
<p class="text-sm leading-6 text-gray-900">{{ data_get($user, 'role.name') }}</p>
</div>
</li>
@endforeach
And let's dive into the results.
As you can see, Livewire no longer refreshes any models because there are no models to be refreshed, just simple arrays with the data we actually want to use.
With this solution, of course you'll lose the ability to call model methods, but nothing stops you from doing whichever logic you need inside the parse
method.
I hope it helps, and thanks for reading!
Posted on February 22, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 22, 2024