Chrip Beyond (Bootcamp) part 5 Pagination
Tags: php, beginners, tutorial, laravel
Published: 2024-01-30 12:09:24

As you have built chirper you may have built up a large amount of chirps, as this number grows chirp index page will start taking longer to load.

We can demonstrait this using [seeders.](https://laravel.com/docs/10.x/seeding)

##Seeders

Seeders allow us to create large amounts of data. In a larger application, you may have a different seeder class for each of the models, but to keep things simple, we will update the included DatabaseSeeder.

In the DatabaseSeeder we can use our model factories to create the data. We want a user with chirps, followers, and users to follow all with their own chirps.

```php
<?php

namespace Database\Seeders;

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

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        User::factory()
            ->has(Chirp::factory()->count(200))
            ->has(
                User::factory()
                    ->count(30)
                    ->has(Chirp::factory()->count(200)),
                'follows'
            )
            ->has(
                User::factory()
                    ->count(30)
                    ->has(Chirp::factory()->count(200)),
                'followers'
            )
            ->createQuietly(['email' => 'test@test.com']);
    }
}
```

We are defining our users email address manually so we can easily login. To run the seeder you can use the command ` php artisan db:seed` or you can run it when refreshing your database by using the command `php artisan migrate:fresh --seed`

If you run one of these commands, and visit the chirp index page, you should find your application start to slow down as it loads the chirps into memory as the page is requested.

Our solution to this problem will be pagination. Pagination allows us to separate the data into different pages.

Laravel provides several methods of [pagination.](https://laravel.com/docs/10.x/pagination#main-content)

As we only need the url for the next page and previous page we can use the simple pagination.

##A new Pagination component

Create a new file in the resources/js/Components called Pagination.vue that will contain links to the next and previous pages.

The component will accept two props, the URL for the next page of results and the URL for the previous page of results.

```vue
<script setup>

import { Link } from '@inertiajs/vue3';

defineProps(['nextUrl', 'prevUrl'])

</script>
<template>
   <nav v-if="prevUrl || nextUrl" class="flex place-content-around bg-white px-4 py-3 sm:px-6 mt-6 rounded-lg" aria-label="Pagination">        <Link v-if="prevUrl" :href="prevUrl" class="relative inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0">Previous</Link>
        <Link v-if="nextUrl" :href="nextUrl" class="relative ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0">Next</Link>
    </nav>
</template>
```

The design of this component is based on one from [Tailwind UI](https://tailwindui.com/components/application-ui/navigation/pagination#component-0797a02a34692167c369d134e7a6f9c5).

##Updating Controllers

Laravel makes using pagination simple by providing eloquent methods.

To use pagination within the Chirp index page, we need to update the ChirpControllers index method. Replace the get() method with the SimplePagination() method.

```diff
 /**
 * Display a listing of the resource.
 */
 public function index(Request $request): Response
 {
     return Inertia::render('Chirps/Index', [
         'chirps' => Chirp::with('user:id,name')
            ->when($request->input('filter') === 'true', fn($q) =>
             $q->whereIn(
                 'user_id',
                 Auth()->user()->follows->pluck('id')
                 ->merge(Auth()->id())
             )
         )
         ->latest()
-         ->get(),
+         ->simplePaginate(25)
        ]);
    }
```

We are now using pagination, but if you return to the Chirp Index page, it will no longer work.

 When using pagination, not only is the collection of models returned but other information too, for example, the next page URL. The collection of models is wrapped around a data property, so we need to update our Chirps/Index.vue page to use this new data property. We can also use the pagination component we created earlier.

```diff
 <script setup>
 import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
 import Chirp from '@/Components/Chirp.vue';
 import InputError from '@/Components/InputError.vue';
 import PrimaryButton from '@/Components/PrimaryButton.vue';
 import Tabs from '@/Components/Tabs.vue';
 import { useForm, Head } from '@inertiajs/vue3';
+ import Pagination from '@/Components/Pagination.vue';

defineProps(['chirps']);

const tabs = [
    {href:route('chirps.index', { filter: 'false'}), active:route().current('chirps.index', { filter: 'false'}), only:['chirps'], text:'All'},
    {href:route('chirps.index', { filter: 'true'}), active:route().current('chirps.index', { filter: 'true'}), only:['chirps'],  text:'Following'},
]

const form = useForm({
    message: '',
});
</script>

<template>
    <Head title="Chirps" />

    <AuthenticatedLayout>
        <div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
            <form @submit.prevent="form.post(route('chirps.store'), { onSuccess: () => form.reset() })">
                <textarea
                    v-model="form.message"
                    placeholder="What's on your mind?"
                    class="block w-full border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"
                ></textarea>
                <InputError :message="form.errors.message" class="mt-2" />
                <PrimaryButton class="mt-4">Chirp</PrimaryButton>
            </form>

            <Tabs :tabs="tabs"/>

            <div class="mt-6 bg-white shadow-sm rounded-lg divide-y">
                <Chirp
-                    v-for="chirp in chirps"
+                    v-for="chirp in chirps.data"
                    :key="chirp.id"
                    :chirp="chirp"
                />
            </div>
+            <Pagination :nextUrl="chirps.next_page_url" :prevUrl="chirps.prev_page_url"/>
        </div>
    </AuthenticatedLayout>
 </template>
```
If you have more than 15 chirps in your app you should now see a next button at the bottom of the page, after clicking it you will see `?page=2` at the end of the URL and a new list of chirps.

There are a couple of improvements we can make to our pagination experience. As you navigate between the pages of chirps, you will notice that the filter query parameter is not carried from the first page. It can be included in the pagination links by adding `->withQueryString()` after the `simplePagination()` method.

If you visit the second page of chirps and then update or delete a Chirp, you will be redirected to the first page of results with no filter query string. To resolve this update the `update()` and `destroy()` methods of the chirpController to not return to a specific route but to return to the request route.

```diff
     /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, Chirp $chirp): RedirectResponse
    {
        $this->authorize('update', $chirp);

        $validated = $request->validate([
            'message' => 'required|string|max:255'
        ]);

         $chirp->update($validated);
-        return redirect(route('chirps.index'));
+        return back();
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Chirp $chirp): RedirectResponse
    {
         $this->authorize('delete', $chirp);

         $chirp->delete();
-        return redirect(route('chirps.index'));
+        return back();
    }
```

If you try again, you will stay on the same page, but you will be at the top of the page. Use the preserveScroll property of the inertia form help and link component inside the Chirp component to stop this.

```diff
 <script setup>
import Dropdown from '@/Components/Dropdown.vue';
import DropdownLink from '@/Components/DropdownLink.vue';
import InputError from '@/Components/InputError.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { Link, useForm } from '@inertiajs/vue3';
import { ref } from 'vue';

dayjs.extend(relativeTime);

const props = defineProps(['chirp']);

const form = useForm({
    message: props.chirp.message,
});

const editing = ref(false);
</script>

<template>
    <div class="p-6 flex space-x-2">
        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600 -scale-x-100" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
            <path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
        </svg>
        <div class="flex-1">
            <div class="flex justify-between items-center">
                <div>
                    <Link :href="route('profile.show', chirp.user.id)" class="text-gray-800 hover:text-gray-500 hover:underline focus:text-gray-500 active:text-gray-900 capitalize">{{ chirp.user.name }}</Link>
                    <small class="ml-2 text-sm text-gray-600">{{ dayjs(chirp.created_at).fromNow() }}</small>
                    <small v-if="chirp.created_at !== chirp.updated_at" class="text-sm text-gray-600"> &middot; edited</small>
                </div>
                <Dropdown v-if="chirp.user.id === $page.props.auth.user.id">
                   <template #trigger>
                        <button>
                           <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
                               <path d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z" />
                           </svg>
                       </button>
                   </template>
                   <template #content>
                        <button class="block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:bg-gray-100 transition duration-150 ease-in-out" @click="editing = true">
                            Edit
                        </button>
-                        <DropdownLink as="button" :href="route('chirps.destroy', chirp.id)" method="delete">
+                        <DropdownLink as="button" :href="route('chirps.destroy', chirp.id)" method="delete" :only="['chirps', 'errors']" preserveScroll>
                            Delete
                        </DropdownLink>
                    </template>
                </Dropdown>
            </div>
-            <form v-if="editing" @submit.prevent="form.put(route('chirps.update', chirp.id), { onSuccess: () => editing = false })">
+            <form v-if="editing" @submit.prevent="form.put(route('chirps.update', chirp.id), { onSuccess: () => editing = false, only:['chirps', 'errors'], preserveScroll:true})">
                <textarea v-model="form.message" class="mt-4 w-full text-gray-900 border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"></textarea>
                <InputError :message="form.errors.message" class="mt-2" />
                <div class="space-x-2">
                    <PrimaryButton class="mt-4">Save</PrimaryButton>
                    <button class="mt-4" @click="editing = false; form.reset(); form.clearErrors()">Cancel</button>
                </div>
            </form>
            <p v-else class="mt-4 text-lg text-gray-900">{{ chirp.message }}</p>
        </div>
    </div>
 </template>
```

##Profile Controller

To use pagination within the `show()` method of our profileController, we must separate retrieving the Chirps and setting their user relationship, we can use a temporary variable to accomplish this.

we can then use the new `$chirps` variable in the `Inertia::render()` method.

```diff
 public function show(User $user): Response
 {
+        $chirps =  $user->chirps()->latest()->simplePaginate(25);
+        $chirps->map(fn (Chirp $chirp) => $chirp->setRelation('user',$user));

        return Inertia::render('Profile/Show',[
            'user' => fn() => $user->loadCount(['followers', 'follows'])->only(['id', 'name', 'created_at', 'followers_count', 'follows_count']),
-            'chirps' => $user->chirps()->latest()->get()->map(fn (Chirp $chirp) => $chirp->setRelation('user',$user)),
+            'chirps' => $chirps,
            'following' => fn () => Auth()->user()->follows()->where('user_id', $user->id)->exists(),
        ]);
 }
```

Update the Profile/Show.vue page to use the now paginated Chirps.

```diff
 <script setup>
 import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
 import Heading from '@/Components/Heading.vue';
 import Chirp from '@/Components/Chirp.vue';
+import Pagination from '@/Components/Pagination.vue';
 import { Head } from '@inertiajs/vue3';

 defineProps(['user','chirps', 'following']);
 </script>

 <template>
     <Head :title="user.name" />

     <AuthenticatedLayout>
         <Heading :user="user" :following="following"/>
         <div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
             <div class="mt-6 bg-white shadow-sm rounded-lg divide-y">
                 <Chirp
-                    v-for="chirp in chirps"
+                    v-for="chirp in chirps.data"
                     :key="chirp.id"
                     :chirp="chirp"
                 />
             </div>
+            <Pagination :nextUrl="chirps.next_page_url" :prevUrl="chirps.prev_page_url"/>
         </div>
     </AuthenticatedLayout>
 </template>
```

##Follow Controller and ListFollowers

We did need to separate the fetching of the data and hiding the pivot data in the followController and ListFollowersController `index` methods.

followController
```diff
 public function index(User $user): Response
 {
        $following = $user
                ->follows()
                ->orderByPivot('created_at','desc')
                ->select('user_id as id', 'name')
                ->addSelect(['following' => function ($query) {
                $query->select('id as following')
                    ->from('follower_user')
                    ->whereColumn('follower_id', Auth()->id())
                    ->whereColumn('user_id', 'users.id');
                }])
-                ->get()->makeHidden('pivot');
+                ->simplePaginate(25, []);
+        $following->map(fn ($user) => $user->makeHidden('pivot'));

        return Inertia::render('Follow/Index', [
            'user' => $user->only(['id', 'name']),
            'users' => fn() => $following,
        ]);

 }
```

listFollowersController
```diff
namespace App\Http\Controllers;

use App\Models\User;
use Inertia\Response;
use Inertia\Inertia;

class ListFollowers extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(User $user): Response
    {
        $followers = $user
            ->followers()
            ->orderByPivot('created_at','desc')
            ->select('follower_id as id', 'name')
            ->addSelect(['following' => function ($query) {
                $query->select('id as following')
                    ->from('follower_user')
                    ->whereColumn('follower_id', Auth()->id())
                    ->whereColumn('user_id', 'users.id');
            }])
-            ->get()->makeHidden('pivot');
+            ->simplePaginate(25, []);
+            $followers->map(fn ($user) => $user->makeHidden('pivot'));

        return Inertia::render('Follow/Index', [
            'user' => $user->only(['id', 'name']),
            'users' => fn() => $followers,
        ]);
    }
}
```

Update the Follow/index.vue page to use the now paginated data.

```diff
 <script setup>
+import Pagination from '@/Components/Pagination.vue';
 import Tabs from '@/Components/Tabs.vue';
 import UserCard from '@/Components/UserCard.vue';
 import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
 import { Link, Head } from '@inertiajs/vue3';

 const props = defineProps(['user', 'users']);

 const tabs = [
     {href:route('followers', props.user.id), active:route().current('followers', props.user.id), text:'Followers'},
     {href:route('follow.index', props.user.id), active:route().current('follow.index', props.user.id), text:'Following'},
 ]

 </script>
 <template>
      <Head>
         <title v-if="route().current('followers')">{{ user.name }} Followers</title>
         <title v-else>{{ user.name }} Follows</title>
      </Head>

     <AuthenticatedLayout>

         <h1 class="bg-white p-6 lg:p-8">
             <Link :href="route('profile.show',user.id)" class="text-2xl font-bold text-gray-900 capitalize hover:text-gray-500 hover:underline focus:text-gray-500 active:text-gray-950">{{ user.name }} </Link>
         </h1>

         <div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
             <Tabs :tabs="tabs"/>
             <div class="divide-y bg-white mt-6 rounded-lg">
                 <UserCard
-                    v-for="user in users"
+                    v-for="user in users.data"
                     :user="user"
                 />
             </div>
+            <Pagination :nextUrl="users.next_page_url" :prevUrl="users.prev_page_url"/>
         </div>
     </AuthenticatedLayout>
 </template>
```

##Testing

if you run the test command `php artisan test` Several tests fail, these tests need updating to take into account the model data is inside the pagination object.

###ChirpControllerTest

```diff
    public function test_index_has_chirps()
    {
        $user = User::factory()->create();

        $chirp = Chirp::factory(1)->create();

        $this->actingAs($user);

        $this->get(route('chirps.index'))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Chirps/Index')
-                ->has('chirps', 1, fn (Assert $page) => $page
-                    ->where('message', $chirp->first()->message)
-                    ->etc()
-                    ->has('user', fn (Assert $page) => $page
-                        ->where('id', $chirp->first()->user_id)
-                        ->where('name', $chirp->first()->user->name)
-                        ->missing('password')
+                ->has('chirps', fn (Assert $page) => $page
+                    ->has('data', 1, fn (Assert $page) => $page
+                        ->where('message', $chirp->first()->message)
+                        ->etc()
+                        ->has('user', fn (Assert $page) => $page
+                            ->where('id', $chirp->first()->user_id)
+                            ->where('name', $chirp->first()->user->name)
+                            ->missing('password')
+                        )
                    )
+                    ->has('next_page_url')
+                    ->has('prev_page_url')
+                    ->etc()
                )
            );
    }

    public function test_only_chirps_by_people_the_user_follows_are_returned_if_filter_is_true()
    {
        $following = User::factory()
            ->has(Chirp::factory())
            ->create();

        $user = User::factory()
            ->hasAttached($following,[],'follows')
            ->create();

        $nonFollowedChirps = Chirp::factory(10)->create();

        $this->actingAs($user)
            ->get(route('chirps.index', ['filter' => 'true']))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Chirps/Index')
-                ->has('chirps', 1, fn (Assert $page) => $page
-                    ->where('message', $following->chirps->first()->message)
-                    ->etc()
-                    ->has('user', fn (Assert $page) => $page
-                        ->where('id', $following->id)
-                        ->where('name', $following->name)
-                        ->missing('password')
-                        ->missing('email')
+                ->has('chirps', fn (Assert $page) => $page
+                    ->has('data', 1, fn (Assert $page) => $page
+                        ->where('message', $following->chirps->first()->message)
+                        ->etc()
+                        ->has('user', fn (Assert $page) => $page
+                            ->where('id', $following->id)
+                            ->where('name', $following->name)
+                            ->missing('password')
+                            ->missing('email')
+                        )
                    )
+                    ->has('next_page_url')
+                    ->has('prev_page_url')
+                    ->etc()
                )
        );
    }
```

We also need to update the `test_chirp_can_be_updated` test and the `test_chirp_can_be_deleted` test as we are now using the `back(`) method to redirect the user, this can be fixed by visiting the Chirp Index page in the test before making the update or delete action.

```diff
 public function test_chirp_can_be_updated()
 {
     $chirp = Chirp::factory()->create();

     $this->actingAs($chirp->user);
+    $this->get(route('chirps.index'));

     $response = $this->patch(route('chirps.update', $chirp),['message' => 'test new message']);

     $this->assertDatabaseHas('chirps', [
         'message' => 'test new message',
     ]);

     $response->assertRedirect(route('chirps.index'));
 }
```
```diff
 public function test_chirp_can_be_deleted()
 {
     $chirp = Chirp::factory()->create();

     $this->assertDatabaseCount('Chirps',1);

     $this->actingAs($chirp->user);
+    $this->get(route('chirps.index'));
     
     $response = $this->delete(route('chirps.destroy',$chirp));

     $response->assertRedirect(route('chirps.index'));

     $this->assertDatabaseEmpty('chirps');
 }
```

###ProfileTest

```diff
public function test_chirps_belonging_to_profile_owner_are_included(): void
    {
        $user = User::factory()
                    ->create();

        $profileUser = User::factory()
                    ->has(User::factory()->count(2),'followers')
                    ->has(User::factory()->count(3),'follows')
                    ->has(Chirp::Factory()->count(5))
                    ->create();

        $this->actingAs($user)
            ->get(route('profile.show',$profileUser->id))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Profile/Show')
                ->has('user', fn (Assert $page) => $page
                    ->where('id', $profileUser->id)
                    ->where('name', $profileUser->name)
                    ->where('followers_count', $profileUser->followers()->count())
                    ->where('follows_count', $profileUser->follows()->count())
                    ->missing('email')
                    ->missing('password')
                    ->etc()
                )
-                ->has('chirps', 5, fn (Assert $page) => $page
-                    ->where('id', $profileUser->chirps()->first()->id)
-                    ->where('message', $profileUser->chirps()->first()->message)
-                    ->etc()
-                    ->has('user', fn (Assert $page) => $page
-                        ->where('id', $profileUser->id)
+                ->has('chirps', fn (Assert $page) => $page
+                    ->has('data', 5, fn(Assert $page) => $page
+                        ->where('id', $profileUser->chirps()->first()->id)
+                        ->where('message', $profileUser->chirps()->first()->message)
                        ->etc()
+                        ->has('user', fn (Assert $page) => $page
+                            ->where('id', $profileUser->id)
+                            ->etc()
+                        )
                    )
+                    ->has('next_page_url')
+                    ->has('prev_page_url')
+                    ->etc()
                )
                ->has('following')
        );
    }

    public function test_chirps_belonging_to_other_uses_are_not_included(): void
    {
        $user = User::factory()
                ->has(User::factory()->count(2),'followers')
                ->has(User::factory()->count(3),'follows')
                ->create();

        $otherUser = User::factory()
                    ->has(Chirp::Factory()->count(5))
                    ->create();

        $this->actingAs($user)
            ->get(route('profile.show',$user->id))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Profile/Show')
                ->has('user', fn (Assert $page) => $page
                    ->where('id', $user->id)
                    ->where('name', $user->name)
                    ->where('followers_count', $user->followers()->count())
                    ->where('follows_count', $user->follows()->count())
                    ->missing('email')
                    ->missing('password')
                    ->etc()
                )
-                ->has('chirps',0)
+                ->has('chirps.data',0)
                 ->has('following')
            );
    }
```

###FollowControllerTest

```diff
 public function test_users_can_see_who_each_other_are_following()
    {
        $user = User::factory()
            ->has(User::factory(3),'follows')
            ->create();

        $otherUser = User::factory()
            ->create();

        $this->actingAs($otherUser)
            ->get(route('follow.index', $user->id))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Follow/Index')
                ->has('user', fn (Assert $page) => $page
                    ->where('id', $user->id)
                    ->where('name', $user->name)
                    ->missing('password')
                    ->missing('email')
                )
-                ->has('users', 3, fn (Assert $page) => $page
-                    ->where('id', $user->follows()->first()->id)
-                    ->where('name', $user->follows()->first()->name)
-                    ->where('following', null)
+                ->has('users', fn (Assert $page) => $page
+                    ->has('data', 3, fn (Assert $page) => $page
+                        ->where('id', $user->follows()->first()->id)
+                        ->where('name', $user->follows()->first()->name)
+                        ->where('following', null)
                    )
+                    ->has('next_page_url')
+                    ->has('prev_page_url')
+                    ->etc()
+                )
            );
    }

    public function test_follow_property_depends_on_auth_user()
    {
        $following = User::factory();

        $user = User::factory()
            ->has($following,'follows')
            ->create();

        $otherUser = User::factory()->create();

        $this->actingAs($otherUser)
            ->get(route('follow.index', $user->id))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Follow/Index')
-                ->has('users', 1, fn (Assert $page) => $page
-                    ->where('id', $user->follows()->first()->id)
-                    ->where('name', $user->follows()->first()->name)
-                    ->where('following', null)
+                ->has('users', fn (Assert $page) => $page
+                    ->has('data', 1, fn (Assert $page) => $page
+                        ->where('id', $user->follows()->first()->id)
+                        ->where('name', $user->follows()->first()->name)
+                        ->where('following', null)
                    )
+                    ->has('next_page_url')
+                    ->has('prev_page_url')
+                    ->etc()
+                )
            );

        $this->actingAs($user)
            ->get(route('follow.index', $user->id))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Follow/Index')
-                ->has('users', 1, fn (Assert $page) => $page
-                    ->where('id', $user->follows()->first()->id)
-                    ->where('name', $user->follows()->first()->name)
-                    ->whereNot('following', null)
+                ->has('users', fn (Assert $page) => $page
+                    ->has('data', 1, fn (Assert $page) => $page
+                        ->where('id', $user->follows()->first()->id)
+                        ->where('name', $user->follows()->first()->name)
+                        ->whereNot('following', null)
                    )
+                    ->has('next_page_url')
+                    ->has('prev_page_url')
+                    ->etc()
+                )
            );

    }
```
ListFollowersTest

```diff
public function test_users_can_see_who_is_following_a_particular_user()
    {
        $user = User::factory()
            ->has(User::factory(3),'followers')
            ->create();

        $otherUser = User::factory()
            ->create();

        $this->actingAs($otherUser)
            ->get(route('followers', $user->id))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Follow/Index')
                ->has('user', fn (Assert $page) => $page
                    ->where('id', $user->id)
                    ->where('name', $user->name)
                    ->missing('password')
                    ->missing('email')
                )
-                ->has('users', 3, fn (Assert $page) => $page
-                    ->where('id', $user->followers()->first()->id)
-                    ->where('name', $user->followers()->first()->name)
-                    ->where('following', null)
+                ->has('users', fn (Assert $page) => $page
+                    ->has('data', 3, fn (Assert $page) => $page
+                        ->where('id', $user->followers()->first()->id)
+                        ->where('name', $user->followers()->first()->name)
+                        ->where('following', null)
                    )
+                    ->has('next_page_url')
+                    ->has('prev_page_url')
+                    ->etc()
+                )
            );
    }

    public function test_follow_property_depends_on_auth_user()
    {
        $follower = User::factory()->create();

        $user = User::factory()->create();

        $user->follows()->attach($follower);
        $user->followers()->attach($follower);

        $otherUser = User::factory()->create();

        $this->actingAs($otherUser)
            ->get(route('followers', $user->id))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Follow/Index')
-                ->has('users', 1, fn (Assert $page) => $page
-                    ->where('id', $user->followers()->first()->id)
-                    ->where('name', $user->followers()->first()->name)
-                    ->where('following', NULL)
+                ->has('users', fn (Assert $page) => $page
+                    ->has('data', 1, fn (Assert $page) => $page
+                        ->where('id', $user->followers()->first()->id)
+                        ->where('name', $user->followers()->first()->name)
+                        ->where('following', NULL)
                    )
+                    ->has('next_page_url')
+                    ->has('prev_page_url')
+                    ->etc()
+                )
            );

        $this->actingAs($user)
            ->get(route('followers', $user->id))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Follow/Index')
-                ->has('users', 1, fn (Assert $page) => $page
-                    ->where('id', $follower->id)
-                    ->where('name', $follower->name)
-                    ->whereNot('following', null)
+                ->has('users', fn (Assert $page) => $page
+                    ->has('data', 1, fn (Assert $page) => $page
+                        ->where('id', $follower->id)
+                        ->where('name', $follower->name)
+                        ->whereNot('following', null)
                    )
+                    ->has('next_page_url')
+                    ->has('prev_page_url')
+                    ->etc()
+                )
            );

    }
```