In this guide, we’re taking a look at how to combine filters, sorting, and infinite scrolling in a Laravel + Inertia.js v2 + Vue 3 app. It’s a simple concept on paper, but the details matter when you need everything working seamlessly together. We’re walking through a real-world property listing scenario, exploring the steps to handle dynamic filtering, sorting, and smooth infinite scrolling in tandem.
Let’s jump right in.
Why This Matters
Infinite scrolling is a great way to load chunks of data without forcing users to click “Next” repeatedly. However, once you throw in dynamic filters (like property types or statuses) and sorting (like “most relevant” or “lowest price”), things can get tricky fast. The good news is that Inertia.js v2 has our back with the new WhenVisible component and Merging props system that makes all of this pretty elegant. We just need to line everything up correctly.
The Laravel Backend
Below is our PropertyController
where we handle requests to display property listings. Even though we’re implementing infinite scrolling, we still use pagination under the hood. That’s because we want to send smaller chunks of data back to the client in a controlled way.
<?php
namespace App\Http\Controllers\Frontend;
use App\Http\Controllers\Controller;
use App\Http\Resources\PropertyResource;
use App\Models\Property;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class PropertyController extends Controller
{
public function index(Request $request): Response
{
$query = Property::all(); // We start by pulling all properties, don't load all in production!
// Apply Filters (property_types, property_status, etc.)
if ($request->filled('property_types')) {
$query->whereIn('property_type_id', $request->input('property_types')); // Filter by type
}
if ($request->filled('property_status')) {
$query->whereIn('property_status_id', $request->input('property_status')); // Filter by status
}
// ... additional filters can go here if you need them
// Apply Sorting (custom method applySort or manually)
$query->applySort($request->input('sort_by')); // We'll assume there's a method on our model to sort
// Paginate (for infinite scrolling, we still use pagination under the hood)
$page = $request->input('page', 1);
$perPage = 12;
$properties = $query->paginate($perPage);
// Determine if this is the first page
$isPageOne = ((int) $page === 1);
return Inertia::render('Properties/Index', [
// On the first page, load the properties directly
// On subsequent pages, use Inertia's merge to append data
'properties' => $isPageOne
? PropertyResource::collection($properties->items())
: Inertia::merge(fn () => PropertyResource::collection($properties->items())),
'currentPage' => fn () => $properties->currentPage(),
'hasMorePages' => fn () => $properties->hasMorePages(),
// Additional data like property types, statuses, etc.
'propertyTypes' => PropertyType::all(), // Assuming you have a PropertyType model, don't load all in production!
'propertyStatus' => PropertyStatus::all(), // Assuming you have a PropertyStatus model, don't load all in production!
]);
}
}
Code Breakdown
- Query and Filter: We pull all property records (
Property::all()
) and then conditionally apply filters based on the request. This is typical “fetch data → apply constraints” logic. - Sort: We call a custom
$query->applySort()
method. This might use something like a match statement inside our model or a dedicated scope to reorder results. - Pagination: Since infinite scrolling is basically fancy pagination, we use
$query->paginate($perPage)
. This keeps our query results manageable. - First Page Check: We figure out if we’re on page one. If so, we load all properties onto the page. If not, we use
Inertia::merge
to append new data to the existing list in the front end.
The Vue 3 Frontend
Now let’s look at our Vue 3 component. This is where the real magic happens: partial reloads with filters, sorting, and infinite scroll triggers.
<script setup lang="ts">
import { ref, computed } from 'vue';
import { usePage, router, WhenVisible } from '@inertiajs/vue3';
import AppLayout from '@/Layouts/AppLayout.vue';
// Types are defined in the `index.d.ts` file for props we get from the controller
const props = defineProps<{
properties: Property[];
currentPage: number;
hasMorePages: boolean;
propertyTypes: PropertyType[];
propertyStatus: PropertyStatus[];
}>();
// sort is defaulted to 'relevant' for our scenario
const sort = ref('relevant');
// Setup an empty filter object that we'll compare against to see if any filters are active
const emptyFilters = computed<Filter>(() => ({
property_types: [],
property_status: [],
sort_by: sort.value,
}));
// Filters is a ref, so we can update it reactively
const filters = ref<Filter>(emptyFilters.value);
// This function applies the user's chosen filters
const applyFilters = (selectedFilters: Filter) => {
filters.value = { ...selectedFilters, sort_by: sort.value };
reload();
}
// Reset filters if they're not already in their default state (empty)
const clearFilters = () => {
// We can use a library like lodash _.isEqual for deep comparison
// but for simplicity, we're using JSON.stringify here
if (JSON.stringify(filters.value) === JSON.stringify(emptyFilters.value)) {
return;
}
filters.value = emptyFilters.value;
reload();
}
// This function is called whenever the user updates the sort method
const applySort = () => {
filters.value = { ...filters.value, sort_by: sort.value };
reload();
}
// Keep track of whether we’re reloading all data (i.e., a full page reload)
const reloadingData = ref(false);
/**
* Reload data from the server with new filters/sorting or to reset to page 1.
*/
const reload = () => {
router.reload({
data: {
...filters.value,
page: 1,
},
preserveUrl: true,
reset: ['properties', 'currentPage', 'hasMorePages'],
onStart: () => {
reloadingData.value = true;
},
onFinish: () => {
reloadingData.value = false;
window.scrollTo(0, 0);
}
});
}
</script>
<template>
<AppLayout title="Property Listings">
<!-- Filter section or any UI elements that calls applyFilters or clearFilters -->
<!-- A sort drop-down form that calls applySort -->
<div class="property-container">
<!-- Render the list of properties -->
<div
v-for="property in properties"
:key="property.id"
class="property-card"
>
{{ property.title }}
</div>
<!-- Infinite scroll trigger -->
<WhenVisible
v-if="hasMorePages"
:params="{
data: {
page: currentPage + 1,
...filters,
},
only: ['properties', 'currentPage', 'hasMorePages'],
preserveUrl: true,
}"
>
<!-- Fallback spinner while fetching -->
<template #fallback>
<div class="text-center my-3">
<span class="spinner-border" role="status"></span>
Loading...
</div>
</template>
<!-- This content shows once the element is visible to the user,
triggering the partial reload for the next page. -->
<div class="text-center my-3">
<span class="spinner-border" role="status"></span>
Loading...
</div>
</WhenVisible>
<!-- Full-page reload spinner (optional) -->
<div v-if="reloadingData" class="full-reload-spinner">
<div class="spinner-overlay">
<span class="spinner-border" role="status"></span>
Loading...
</div>
</div>
</div>
</AppLayout>
</template>
<style scoped>
.property-container {
display: grid;
gap: 1rem;
}
.property-card {
border: 1px solid #ddd;
padding: 1rem;
}
.full-reload-spinner {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
</style>
Code Breakdown
- Props: We accept the paginated properties, plus the current page number,
hasMorePages
, and any other references likepropertyTypes
orpropertyStatus
. - Reactive Filters: We hold our filter state in
filters
, which is initially empty (or matches the default sorting). When users pick a filter, we update this object. - Reload: Instead of pushing a new page entirely, we use
router.reload()
withpartial reload
logic. This means we only refetch data for properties,currentPage
, andhasMorePages
. - Infinite Scroll: We use
<WhenVisible>
with the:params
prop to load the next page in the background once the user scrolls far enough. - Conditional UI: We display a fallback spinner as soon as the user triggers the load for more data. Meanwhile, if we do a full reload (like changing filters or sorting), we also show a full-page overlay.
Putting It All Together
- User Hits the Page
We load the initial chunk of properties from the server. If there are more pages, the<WhenVisible>
component appears at the bottom and triggers the next chunk when scrolled into view. - Filtering & Sorting
Whenever we apply new filters or change the sorting order, we call ourreload()
method. That method reloads the page 1 results with updated filters, resetting the scroll, and if there are more pages, the infinite scrolling mechanism picks up again. - Partial Reloads
By default, Inertia would re-render the entire page, but we only need new data. Thanks to Inertia’s partial reload feature and theonly: ['properties', 'currentPage', 'hasMorePages']
setting, we’re just pulling in updated items, current page, and pagination flags. Everything else remains untouched. - Keeping It Smooth
On the UX side, we’re adding spinners in two places: for the infinite scroll’s next page loading and for the full-page reload triggered by filter changes. This makes sure the user always knows something is happening.
Final Thoughts
That’s the full workflow. By combining pagination on the backend, partial reloads on the front end, and the new WhenVisible component and Merging props system in Inertia.js v2, we can elegantly handle infinite scrolling while still letting our users fine-tune their filters and sorting. It’s one of those features that feels magical when done right—sort our properties, see relevant ones instantly, and keep on scrolling.
Thanks for sticking around. Hopefully, this guide helps you level up your Inertia.js v2 knowledge. I'm a big fans of the “it just works” approach. But with a little bit of extra code, we get a super smooth, user-friendly listing experience.
Stay inspired, keep building!
Author Of article : Deon Okonkwo Read full article