Server-Side Pagination in Livewire with the WithPagination Trait
Livewire's WithPagination trait gives you server-side pagination with SPA-like updates. Here's the setup, the page-reset bug to avoid, and the payload cost to measure.
12 Aug 2026, 09:40 UTC

When a Laravel list page grows from fifty rows to fifty thousand, rendering everything at once stops being an option. The usual alternative—hand-rolled AJAX pagination—means writing endpoints, managing query strings, and re-rendering fragments yourself. Livewire's WithPagination trait removes most of that plumbing: you keep a standard Laravel paginator on the server, and Livewire swaps just the list markup in the browser when the user clicks a page link.
This post walks through setting it up, the one state bug almost everyone hits, and the payload trade-off you should measure before raising your page size.
Adding the trait to a component
Pagination in Livewire 3 builds on Laravel's existing paginator. Add the trait to your component and call paginate() on an Eloquent query inside render(), exactly as you would in a controller:
// app/Livewire/PostIndex.php
namespace App\Livewire;
use App\Models\Post;
use Livewire\Component;
use Livewire\WithPagination;
class PostIndex extends Component
{
use WithPagination;
public function render()
{
return view('livewire.post-index', [
'posts' => Post::latest()->paginate(10),
]);
}
}The trait tracks the current page, keeps it in the URL query string, and intercepts clicks on the pagination links so they issue a Livewire request instead of a full navigation.
Rendering the list and links
The Blade view looks like ordinary Laravel pagination. Loop over the paginated items and call links() where you want the controls:
{{-- resources/views/livewire/post-index.blade.php --}}
<div>
<ul>
@foreach ($posts as $post)
<li>{{ $post->title }}</li>
@endforeach
</ul>
{{ $posts->links() }}
</div>Because the whole component re-renders on each request, keep the markup inside the component's single root element and let Livewire's DOM diffing handle the update.
The page-reset bug with search filters
The classic failure mode: a user navigates to page 5, then types in a search box that narrows results to a single page. The component still requests page 5 of a one-page result set and renders an empty list. The fix is to reset the page whenever filter inputs change, using Livewire's updated lifecycle hook:
public string $search = '';
public function updatedSearch(): void
{
$this->resetPage();
}
public function render()
{
return view('livewire.post-index', [
'posts' => Post::query()
->when($this->search, fn ($q) =>
$q->where('title', 'like', "%{$this->search}%"))
->latest()
->paginate(10),
]);
}resetPage() is provided by the trait. Any property that affects the query—search terms, status filters, sort order—needs the same treatment, either via individual updated{Property} hooks or a single updated() method that checks which property changed.
The payload trade-off
Server-side pagination keeps the database query cheap, but Livewire serializes component state on every round trip. A large perPage value, or models carrying big text columns, inflates the HTML diff and the response size. Ten to twenty-five rows per page is a sensible default; if you are tempted to go higher, measure first.
Two related limitations worth knowing:
- Third-party JavaScript: libraries that initialize on page load (data tables, charts, syntax highlighters) will not re-run when Livewire patches the DOM. Wrap those regions in
wire:ignoreor re-initialize them from Alpine.js event hooks after updates. - External URL changes: if something outside Livewire rewrites the page query parameter, component state can drift from the URL. Let Livewire own the
pageparameter rather than editing it manually.
Verifying it works
Open your browser's developer tools, switch to the Network tab, and click a pagination link. You should see a single Livewire update request (a POST to the Livewire message endpoint) whose response contains the new page's markup—no full document load, no flicker. While you are there, compare the response size at paginate(10) versus paginate(100); that difference is the concrete cost of larger pages, and it is the number to watch as your tables grow.
If you are starting fresh, php artisan make:livewire PostIndex plus the trait above is all the scaffolding you need. Add the filter-reset hook before your users find the empty-page bug for you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.