livewire/livewire: Fix: cursor pagination no longer crashes on an empty page
Contributed by joshhanley
Part of the livewire v4.4.7 release
Cursor pagination in Livewire 4 no longer crashes when it lands on an empty page. If you are upgrading from v3.8.10 and your components use WithPagination with cursorPaginate(), there was a class of states — being on page two or later when the result set becomes empty — that threw an error instead of rendering. That includes deleting records, a filter or search narrowing results to nothing, or a user opening a stale or bookmarked cursor URL. Pull request #10302 fixes the crash in Livewire's built-in simple pagination views and adds regression tests covering both empty-page directions across both pagination themes.
The crash: an empty page that isn't the first
Consider a typical cursor-paginated component:
<?php
use App\Models\User;
use Livewire\Component;
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public function deleteLatest()
{
User::where('id', '>', 3)->delete();
}
public function with()
{
return ['users' => User::orderBy('id')->cursorPaginate(3, ['*'], 'page')];
}
}; ?>
<div>
<button type="button" wire:click="deleteLatest">Delete latest</button>
@foreach ($users as $user)
<p wire:key="user-{{ $user->id }}">{{ $user->name }}</p>
@endforeach
{{ $users->links() }}
</div>
With three users per page, click through to page two, then press "Delete latest". The result set drops to three records — exactly one page — so your current cursor now points past the data. On the next render, Livewire throws an error instead of showing the empty page with its pagination controls.
Why it happened
The simple pagination views call ->encode() directly on the cursor:
wire:click="setPage('{{ $paginator->previousCursor()->encode() }}', '{{ $paginator->getCursorName() }}')"
previousCursor() and nextCursor() are derived from the current page's items, so on an empty page both return null. The problem is that the buttons still render in that state: the "Previous" button shows whenever onFirstPage() is false (true on any page but the first), and "Next" shows whenever hasMorePages() is true. Put together, an empty non-first page renders a button whose only job is to call ->encode() on null — hence the crash.
The fix: fall back to the current cursor
The views now capture the cursor and fall back to the paginator's current cursor when it is null:
@php($previousCursor = $paginator->previousCursor() ?? $paginator->cursor())
<button wire:click="setPage('{{ $previousCursor?->encode() }}', '{{ $paginator->getCursorName() }}')">
Previous
</button>
This mirrors how Laravel's own pagination views behave. They never dereference the cursor directly; they navigate by URL through the null-safe previousPageUrl() / nextPageUrl() helpers:
public function previousPageUrl()
{
if (is_null($previousCursor = $this->previousCursor())) {
return null;
}
return $this->url($previousCursor);
}
When there is no cursor, Laravel renders an empty href="", which simply reloads the same page. Livewire navigates via wire:click with an encoded cursor string rather than a URL, so it needs something to encode — falling back to the current cursor reproduces the same "reload the page you're on" behavior. Since a cursor points at the position you're already viewing, re-submitting it re-renders the same (empty) page instead of erroring.
The change is applied to both simple-tailwind.blade.php and simple-bootstrap.blade.php, for both the previous and next links, with regression tests for a forward cursor (stale or bookmarked, pointing past the data) and a backward cursor (clicking "previous" onto an empty page) across both themes.
Upgrade impact
For most apps there is nothing to do: if you render pagination through the built-in $paginator->links() views, the fix applies automatically and empty cursor pages now render normally.
Two things are worth knowing:
-
Published views need a manual update. If you have published and customized the simple pagination views, your copies override the fixed framework views. Port the same fallback into your published
simple-tailwind.blade.php/simple-bootstrap.blade.php:@php($previousCursor = $paginator->previousCursor() ?? $paginator->cursor()) @php($nextCursor = $paginator->nextCursor() ?? $paginator->cursor())and use
{{ $previousCursor?->encode() }}/{{ $nextCursor?->encode() }}in thewire:clickandwire:keyattributes. -
Small behavior change. On an empty page, the Previous/Next buttons now re-load the current (empty) page instead of crashing — matching Laravel's behavior for its own views.
Since the change is tagged for the 4.x line, it ships as part of the v4 you are upgrading into rather than a 3.x patch — one less crash between 3.8.10 and v4 for cursor-paginated components.
Takeaways
- Cursor pagination no longer throws when a cursor lands on an empty, non-first page.
- Root cause:
previousCursor()andnextCursor()arenullon empty pages, but the simple views called->encode()on them unconditionally. - The fix falls back to
$paginator->cursor(), so the buttons re-load the same page, mirroring Laravel's null-safe URL helpers. - If you published custom copies of the simple pagination views, port the fallback change into them.