Local Pagination and Filtering
Problem
You have an in-memory dataset you need to paginate, filter, and sort without sending requests to a server. The filtering and sort logic change at runtime based on user input, and page state must reset correctly when filters change.
Solution
Use createLocalSource(). The source owns page state; calling setFilter() or setSort() resets to page 1 automatically.
ts
import { createLocalSource } from '@vielzeug/sourcerer';
type Product = { id: number; name: string; price: number };
const products: Product[] = [
{ id: 1, name: 'Apple', price: 2 },
{ id: 2, name: 'Banana', price: 1 },
{ id: 3, name: 'Orange', price: 3 },
{ id: 4, name: 'Apricot', price: 4 },
];
const source = createLocalSource(products, { limit: 2 });
// Filter to items whose name contains 'ap' (case-insensitive)
await source.setFilter((p) => p.name.toLowerCase().includes('ap'));
// Sort by price ascending
await source.setSort((a, b) => a.price - b.price);
console.log(source.current);
// [{ id: 1, name: 'Apple', price: 2 }, { id: 4, name: 'Apricot', price: 4 }]
console.log(source.meta.totalItems); // 2 (only items matching the filter)Apply filter and sort without double recompute
Call setFilter() and setSort() in sequence — the second call runs after the first has already committed. For a single atomic recompute, apply both directly in the LocalConfig when constructing the source:
ts
const source = createLocalSource(products, {
limit: 2,
filter: (p) => p.name.toLowerCase().includes('ap'),
sort: (a, b) => a.price - b.price,
});Or use the async config hook pattern with filterAsync if your predicates are expensive:
ts
const source = createLocalSource(products, {
limit: 2,
filterAsync: async (items, signal) => items.filter((p) => p.name.toLowerCase().includes('ap')),
sortAsync: async (items, signal) => [...items].sort((a, b) => a.price - b.price),
});Compose predicates
Inline composition keeps things explicit and avoids external dependencies:
ts
await source.setFilter((p) => p.name.toLowerCase().includes('a') && p.price <= 3);Pitfalls
- Calling
setFilter()andsetSort()separately triggers two recomputes and two subscriber notifications. Usepatch({ filter, sort })instead to apply both in a single recompute with one notification. - The default
searchFnuses fuzzy matching. For exact substring matching in tests or precise UIs, provide a customsearchFn:tscreateLocalSource(data, { searchFn: (items, q) => items.filter((item) => item.name.toLowerCase().includes(q.toLowerCase())), });