Why Sourcerer?
Managing paginated lists across local and remote data usually means writing different state models for each case, wiring separate loading flags, and duplicating URL serialization logic. Sourcerer provides one typed contract — current, meta, and subscribe — that works the same whether data lives in memory or comes from a server.
ts
// Without Sourcerer — manual local list state
const [items, setItems] = useState(allUsers);
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const pageSize = 10;
const filtered = items.filter((u) => u.name.includes(search));
const paginated = filtered.slice((page - 1) * pageSize, page * pageSize);
const totalPages = Math.ceil(filtered.length / pageSize);
// ... repeat for remote with loading/error/abort logic ...
// With Sourcerer — same API for both
const source = createLocalSource(allUsers, { limit: 10 }); // or createRemoteSource(...)
await source.search(search, { immediate: true });
// source.current, source.meta.pageCount — both cases handled| Feature | Sourcerer | TanStack Query | SWR |
|---|---|---|---|
| Bundle size | 5.2 KB | ~16 kB | ~6 kB |
| In-memory source primitive | |||
| Remote source primitive | |||
| Cursor-based pagination | Partial | Partial | |
| Infinite scroll source | |||
| Typed page/filter/sort/search model | Partial | Partial | |
| Optimistic updates | |||
| URL query encode/decode helpers | Partial | Partial | |
| Framework agnostic |
Use Sourcerer when you want one typed source abstraction for both local collections and server-backed lists, with built-in support for pagination, search, filters, and optimistic mutation.
Consider TanStack Query when your data layer is already built around query keys, cache invalidation across many components, and React-first DevTools integration.
Installation
sh
pnpm add @vielzeug/sourcerersh
npm install @vielzeug/sourcerersh
yarn add @vielzeug/sourcererQuick Start
ts
import { createLocalSource } from '@vielzeug/sourcerer';
const source = createLocalSource(
[
{ id: 1, name: 'Ada' },
{ id: 2, name: 'Grace' },
{ id: 3, name: 'Linus' },
],
{ limit: 2 },
);
await source.search('ada', { immediate: true });
console.log(source.current); // [{ id: 1, name: 'Ada' }]
console.log(source.meta.pageNumber); // 1ts
import { createRemoteSource } from '@vielzeug/sourcerer';
const source = createRemoteSource({
fetch: async ({ filter, limit, page, search, sort }, signal) => {
const res = await fetch(`/api/users?page=${page}&limit=${limit}`, { signal });
return res.json(); // { items: User[], total: number }
},
limit: 20,
});
// autoFetch is true by default — initial data loads immediately
await source.ready();
console.log(source.current, source.meta.totalItems);Features
| Factory | Data model | Navigation | Key extras |
|---|---|---|---|
createLocalSource() | In-memory array | Page number | filterAsync, sortAsync, patch(), custom searchFn, ready() |
createRemoteSource() | Server fetch | Page number | staleTime, optimisticUpdate, patch(), ready(), queryKey |
createCursorSource() | Server fetch | Cursor tokens | patch(), ready(), queryKey |
createInfiniteSource() | Server fetch | Append (loadMore) | patch(), loadedPages, ready(), queryKey |
Documentation
See Also
- Ripple — reactive signals; Sourcerer's loading, error, and data state are exposed as signals for framework-agnostic UI binding
- Arsenal — utility functions used inside Sourcerer's fetch and transform pipelines
- Wayfinder — client-side router; sync Sourcerer's pagination and filter state with URL search params