# Laravel and Vue pagination

> paginate, simplePaginate, and cursorPaginate return different JSON. The Vue page can only render the links that object actually has.

- URL: https://laravel-vuejs.space/laravel-vue-pagination
- Author: Anass Ez-zouaine (https://laravel-vuejs.space/author/anass-ez-zouaine)
- Published: 2026-10-10
- Topics: Laravel, VueJs, Inertia

You switch `paginate` to `simplePaginate` for speed and the footer shows “Page 2 of undefined”. Each Laravel paginator sends a different payload. Here is what each one returns and a Vue footer that works with all of them.

> **TL;DR**
>
> -   `paginate` counts (has `total`, `last_page`); `simplePaginate` and `cursorPaginate` do not.
> -   `cursorPaginate` needs a unique order column: order by `id`.
> -   Render Previous/Next from `prev_page_url` / `next_page_url`; they are `null` at the ends.
> -   Add `withQueryString()` so filters survive the next page.
>
> **What you'll learn**
>
> -   What `paginate`, `simplePaginate` and `cursorPaginate` return
> -   A Vue footer that works for all three
> -   How to lock the shape with a test
>
> **Who it's for:** Laravel + Vue developers with growing lists
>
> **Applies to:** Laravel 13 · Inertia 3 · Vue 3 · Pest

The [CRUD guide](https://laravel-vuejs.space/laravel-inertia-vue-crud) loads posts with `paginate(10)`. That is the right call when the list is small enough to count. This page is the other two paginators, and the Vue markup that breaks when you swap them without reading the payload.

If you are still choosing Inertia, a separate SPA, or Nuxt, start with [the stack guide](https://laravel-vuejs.space/laravel-vue-in-2026). Sorting the same list is the [table guide](https://laravel-vuejs.space/laravel-vue-datatable). Do not call `Post::all()` and paginate in the browser.

## What each method returns

| Method | Count query | `total` / `last_page` | URL |
| --- | --- | --- | --- |
| `paginate(10)` | Yes | Yes | `?page=2` |
| `simplePaginate(10)` | No | No | `?page=2` |
| `cursorPaginate(10)` | No | No | `?cursor=…` |

`paginate(10)` runs a count query. The JSON includes `total`, `last_page`, `current_page`, `prev_page_url`, and `next_page_url`. The CRUD page can say “Page 2 of 6” because those keys exist.

`simplePaginate(10)` skips the count. You get `current_page` and the previous and next URLs. There is no `total` and no `last_page`. Use it when the table is large and the footer only needs Previous and Next.

> **Gotcha: cursors need a unique order**
>
> `cursorPaginate(10)` also skips the count. Instead of `?page=2` it uses a `cursor` string and returns `next_page_url` and `prev_page_url`. There is no `last_page`. It needs a unique order column, or a unique combination of columns. Two rows with the same `published_at` do not give the cursor a stable bound, so order by `id`. Skip and repeat when rows are inserted or deleted between pages is the offset case, `paginate` and `simplePaginate`.

`app/Http/Controllers/PostController.php`
```php
$posts = Post::query()
    ->latest('id')
    ->cursorPaginate(10);
```

Swap `cursorPaginate` for `simplePaginate` when you want page numbers without a total. Keep `paginate` from the CRUD guide when the count is cheap and the UI shows it.

## The Vue footer

Read `posts.data` for the rows, the same as the CRUD page. Stop reading `last_page` unless you called `paginate()`.

`resources/js/pages/posts/Index.vue`
```vue
<nav class="flex gap-4">
  <Link v-if="posts.prev_page_url" :href="posts.prev_page_url">Previous</Link>
  <Link v-if="posts.next_page_url" :href="posts.next_page_url">Next</Link>
</nav>
```

`prev_page_url` and `next_page_url` are `null` on the ends. The `v-if` is the whole empty state. A cursor page and a simple page can share this footer. A counted page can add the current page and `last_page` beside it.

> **Tip: keep the filter on page 2**
>
> `withQueryString()` still matters once the list has a search box. Without it, Next drops `?search=` and the second page is the unfiltered table.

```php
Post::query()
    ->when($search !== '', fn ($query) => $query->where('title', 'like', "%{$search}%"))
    ->latest('id')
    ->simplePaginate(10)
    ->withQueryString();
```

## A test that locks the shape

> **Fail fast if someone swaps the paginator**
>
> `tests/Feature/PostsPaginationTest.php`
> ```php
> it('does not claim a last page it did not count', function () {
>     Post::factory()->count(3)->create();
>
>     $this->get('/posts')
>         ->assertOk()
>         ->assertInertia(fn ($page) => $page
>             ->has('posts.data', 3)
>             ->missing('posts.last_page')
>             ->missing('posts.total')
>         );
> });
> ```

Point that test at whichever method you chose. `missing('posts.total')` fails if someone “helpfully” switches the controller back to `paginate()` while the Vue page was written for a simple paginator.

## Where this stops

Loading the next page by appending rows, instead of replacing them, is a partial reload. That is [partial reloads](https://laravel-vuejs.space/inertia-partial-reloads). The form, the search box, and `paginate(10)` stay in the [CRUD guide](https://laravel-vuejs.space/laravel-inertia-vue-crud).

## Recap

- Pick the paginator by whether you can afford (and show) a count.
- One Previous/Next footer works for all three.
- Order cursor pages by a unique column.
- `withQueryString()` whenever there is a filter.

## FAQ

**Q: Why is last_page undefined in my Vue pagination?**

You are using `simplePaginate` or `cursorPaginate`, which skip the count. Use `paginate` or drop `last_page` from the footer.

**Q: When should I use cursorPaginate in Laravel?**

For large, frequently changing lists where offset pages skip or repeat rows. Order by a unique column such as `id`.

**Q: Why does page 2 lose my search filter?**

Add `->withQueryString()` to the paginator so the links keep `?search=`.

## Next steps

- [CRUD with Inertia and Vue](https://laravel-vuejs.space/laravel-inertia-vue-crud)
- [A sortable table in Laravel and Vue](https://laravel-vuejs.space/laravel-vue-datatable)
- [Inertia partial reloads](https://laravel-vuejs.space/inertia-partial-reloads)

## Sources

- [Laravel 13: Pagination](https://laravel.com/docs/13.x/pagination)
- [Laravel 13: Cursor pagination](https://laravel.com/docs/13.x/pagination#cursor-pagination)
