Reactive Controls with Stateit
Problem
Multiple UI elements — a search input, a role filter dropdown, a sort toggle — all control the same list. Without shared state, each control needs its own callback wired to the source, and keeping them in sync requires threading state manually.
Solution
Store filter, search, and sort settings in a Stateit store and use effect() to project them into the source whenever any value changes.
ts
import { effect, store } from '@vielzeug/stateit';
import { createLocalSource, sortBy } from '@vielzeug/sourceit';
type User = { id: number; name: string; role: 'admin' | 'member' };
const users: User[] = [
{ id: 1, name: 'Ada Lovelace', role: 'admin' },
{ id: 2, name: 'Grace Hopper', role: 'admin' },
{ id: 3, name: 'Linus Torvalds', role: 'member' },
{ id: 4, name: 'Margaret Hamilton', role: 'member' },
];
const source = createLocalSource(users, { limit: 2 });
const controls = store({
query: '',
role: 'all' as 'all' | 'admin' | 'member',
sort: 'name' as 'name' | 'role',
});
// Reactively project shared UI state into the source model
effect(() => {
source.batch((ctx) => {
ctx.search(controls.value.query);
ctx.setFilter(
controls.value.role === 'all'
? undefined
: (user) => user.role === controls.value.role,
);
ctx.setSort(
controls.value.sort === 'name'
? sortBy((u) => u.name, 'asc')
: sortBy((u) => u.role, 'asc'),
);
});
});
// Any update to controls automatically re-filters/sorts/paginates the source
controls.patch({ query: 'a' });
controls.patch({ role: 'admin' });
console.log(source.current); // filtered + sorted page 1
console.log(source.meta.totalItems);Pitfalls
- The
effect()runs immediately on creation. If the source has not yet loaded its initial data, the first filter application fires before data is available. Initialize the source before creating the effect. - Calling
source.batch()inside the effect is required. Without it, eachctx.search()andctx.setFilter()call resets pagination independently, causing multiple intermediate re-renders. - Modifying
controlsinside theeffect()callback creates an infinite reactive loop. Only read fromcontrolsinside the effect; write to it from UI event handlers.