Infinite Scrolling with Livewire v3 + AlpineJS without impacting performance
Tags: laravel, livewire, php, performance
Published: 2024-02-22 18:18:40

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:

```php
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:

```html
@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:

First page:
![Issue first page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3yav5sfa8x84sv2v59rw.png)

Scrolled to the end of the list:
![Issue last page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3z9rwzh3wdizqo89pahd.png)
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.

```php
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:

```html
@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.

First page:
![Solution first page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7xg3ws85fu5ucb6jo8tp.png)

Second page and so on:
![Solution second page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6b0c5n0ujk3wpz0ee129.png)

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!