Reader Stacks

Building AJAX Pagination With Search in Laravel

Laravel's own pagination links work over AJAX with one small adjustment — intercepting their click events instead of letting the browser navigate to them directly.

Combining Laravel's built-in pagination with AJAX — so clicking a page link updates the results without a full page reload — needs surprisingly little custom code, since Laravel's own pagination links already work correctly, they just need their click behavior intercepted.

The controller, returning both the initial view and AJAX partial

public function index(Request $request)
{
    $products = Product::query()
        ->when($request->filled('search'), function ($query) use ($request) {
            $query->where('name', 'like', '%'.$request->search.'%');
        })
        ->paginate(10)
        ->withQueryString();

    if ($request->ajax()) {
        return view('products.partials.list', compact('products'))->render();
    }

    return view('products.index', compact('products'));
}

$request->ajax() checks for the same header jQuery sets automatically on AJAX calls — this single check is what lets the same controller method serve both the full initial page load and subsequent AJAX-only partial updates from one method, without duplicating the query logic.

The partial view containing just the list and pagination

{{-- resources/views/products/partials/list.blade.php --}}
@foreach ($products as $product)
    
{{ $product->name }}
@endforeach {{ $products->links() }}

The main view, including the partial

{{-- resources/views/products/index.blade.php --}}


@include('products.partials.list')

Intercepting pagination link clicks with AJAX

function loadProducts(url) {
    $.get(url, { search: $('#search').val() }, function (response) {
        $('#product-list').html(response);
    });
}

$(document).on('click', '#product-list .pagination a', function (event) {
    event.preventDefault();
    loadProducts($(this).attr('href'));
});

let searchTimeout;
$('#search').on('input', function () {
    clearTimeout(searchTimeout);
    searchTimeout = setTimeout(() => loadProducts('/products'), 300);
});

Using $(document).on('click', '#product-list .pagination a', ...) (event delegation) rather than binding directly to the pagination links is essential here — the links themselves get replaced every time #product-list's content is swapped in, so a direct binding would only work for the very first page load and silently stop working after that.

Why the debounce on the search input matters

The setTimeout/clearTimeout pattern on the search input delays the actual request until the user pauses typing for 300ms — without this debounce, every single keystroke would fire its own request, creating unnecessary server load and a flickering, race-condition-prone result list as multiple overlapping requests return out of order.

Making Laravel's pagination links generate correctly for this pattern

withQueryString() in the controller ensures the pagination links themselves include the current search term as a query parameter — without it, clicking to page 2 of a filtered search would silently lose the active search term and show unfiltered page 2 results instead.