@mits_pl/use-inertia-filters
v1.0.2
Published
Headless Vue 3 composable for managing filter state with Inertia.js — debounced, URL-synced, TypeScript-first.
Maintainers
Readme
@mits_pl/use-inertia-filters
Headless Vue 3 composable for managing filter state with Inertia.js — debounced, URL-synced, TypeScript-first.
Built and maintained by MITS — a software house specializing in Laravel + Vue + Inertia.
The problem
Every Laravel + Inertia app has the same boilerplate: a list page with a search input, a status dropdown, a per-page selector. Every time you need to:
- Keep filter state in sync with the URL query string
- Debounce the search input so you don't hammer the server
- Fire immediately for sort/per_page changes
- Preserve Vue component state so the page doesn't flicker
- Reset filters back to defaults
This composable does all of that in one line.
Installation
npm install @mits_pl/use-inertia-filters
# or
yarn add @mits_pl/use-inertia-filters
# or
pnpm add @mits_pl/use-inertia-filtersPeer dependencies: vue >= 3.3, @inertiajs/vue3 >= 1.0
Quick start
<script setup lang="ts">
import { useInertiaFilters } from '@mits_pl/use-inertia-filters'
const { filters, reset, isDirty } = useInertiaFilters({
search: '',
status: null,
perPage: 15,
})
</script>
<template>
<div>
<input v-model="filters.search" placeholder="Search..." />
<select v-model="filters.status">
<option :value="null">All</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<select v-model="filters.perPage">
<option :value="15">15</option>
<option :value="25">25</option>
<option :value="50">50</option>
</select>
<button v-if="isDirty()" @click="reset">Clear filters</button>
</div>
</template>That's it. Changes are debounced (300ms by default), the URL is updated, and the page re-renders with fresh data from the server.
How it works
- On mount, the composable reads the current URL query string and hydrates the filter state (with type casting based on initial values).
- When any filter changes, a debounced
router.get()is called with only the non-default, non-empty values in the query string — keeping the URL clean. - The visit uses
preserveState: trueandreplace: trueby default, so the page updates without a flicker and without polluting browser history.
API
useInertiaFilters(initialFilters, options?)
initialFilters
A plain object defining your filter keys and their default values. Types are inferred from the initial values and used for URL hydration casting.
useInertiaFilters({
search: '', // string
status: null, // null | string
page: 1, // number — cast from URL string automatically
archived: false, // boolean — cast from URL string automatically
})options
| Option | Type | Default | Description |
|---|---|---|---|
| debounce | number | 300 | Debounce delay in ms |
| preserveState | boolean | true | Preserve Vue component state |
| preserveScroll | boolean | true | Preserve scroll position |
| replace | boolean | true | Replace history entry (no back-button spam) |
| debounceOnly | string[] | undefined | Only debounce these keys; all others fire immediately |
| onBeforeVisit | (filters) => boolean \| void | — | Return false to cancel the visit |
| onAfterVisit | (filters) => void | — | Called after each visit |
Return value
| Property | Type | Description |
|---|---|---|
| filters | Reactive<T> | Reactive filters object — bind with v-model |
| reset() | () => void | Reset all filters to initial values |
| resetKeys(...keys) | (...keys) => void | Reset specific keys only |
| flush() | () => void | Trigger visit immediately, bypassing debounce |
| isDirty() | () => boolean | True if any filter differs from its initial value |
| isKeyDirty(key) | (key) => boolean | True if the specific key differs from initial |
Real-world example with debounceOnly
You want the search field to be debounced (wait for the user to stop typing), but perPage and sort should fire instantly.
<script setup lang="ts">
import { useInertiaFilters } from '@mits_pl/use-inertia-filters'
const { filters, reset, isDirty } = useInertiaFilters(
{
search: '',
status: null,
sort: 'name',
direction: 'asc',
perPage: 15,
},
{
debounce: 400,
debounceOnly: ['search'], // only search is debounced
},
)
</script>Laravel controller
Nothing special needed on the backend — standard Inertia pattern:
public function index(Request $request): Response
{
$users = User::query()
->when($request->search, fn ($q, $v) => $q->where('name', 'like', "%{$v}%"))
->when($request->status, fn ($q, $v) => $q->where('status', $v))
->orderBy($request->sort ?? 'name', $request->direction ?? 'asc')
->paginate($request->perPage ?? 15)
->withQueryString();
return Inertia::render('Users/Index', [
'users' => $users,
'filters' => $request->only('search', 'status', 'sort', 'direction', 'perPage'),
]);
}Why not use an existing table package?
Packages like inertiajs-tables-laravel-query-builder are great but opinionated:
- Require Spatie Laravel Query Builder on the backend
- Require Tailwind CSS Forms plugin
- Bring their own table UI components
useInertiaFilters is headless. No UI, no backend requirements, no CSS. Use it with any component library — shadcn-vue, PrimeVue, Vuetify, Quasar, or plain HTML.
TypeScript
Full type inference out of the box:
const { filters, isKeyDirty } = useInertiaFilters({
search: '',
perPage: 15,
})
filters.search // string ✓
filters.perPage // number ✓
filters.nope // TS error ✓
isKeyDirty('search') // ✓
isKeyDirty('nope') // TS error ✓License
MIT © MITS Sp. z o.o.
