Create a Local Source
ts
import { createLocalSource } from '@vielzeug/sourceit';
type User = { id: number; name: string; role: 'admin' | 'user' };
const source = createLocalSource<User>(users, { limit: 10 });Local mutations
ts
source.setFilter((user) => user.role === 'admin');
source.setSort((a, b) => a.name.localeCompare(b.name));
source.searchNow('ada');
source.goTo(2);Atomic updates
Use batch() when multiple changes should apply together.
ts
source.batch((ctx) => {
ctx.setFilter((u) => u.role === 'user');
ctx.search('lin');
ctx.setLimit(5);
});Create a Remote Source
ts
import { createRemoteSource } from '@vielzeug/sourceit';
type User = { id: number; name: string };
type UserFilter = { role?: 'admin' | 'user' };
type UserSort = { by: 'name' | 'id'; dir: 'asc' | 'desc' };
const source = createRemoteSource<User, UserFilter, UserSort>({
fetch: async ({ filter, limit, page, search, sort }) => {
const result = await api.users.list({ filter, limit, page, search, sort });
return { items: result.items, total: result.total };
},
filter: { role: 'user' },
sort: { by: 'name', dir: 'asc' },
limit: 25,
});Remote lifecycle
ts
source.refresh();
await source.ready();
source.search('ada');
source.commit();
await source.ready();Read model
Both local and remote sources expose:
current: the current page itemsmeta: pagination and status info (pageNumber,pageCount,totalItems,isLoading,errorMessage)toQuery(): serializable state for URL/state sync
ts
const { current, meta } = source;
console.log(current.length, meta.pageNumber, meta.totalItems);URL Query Param Sync
ts
import { encodeLocalQueryParams } from '@vielzeug/sourceit';
const params = encodeLocalQueryParams(source.toQuery());
// -> { page: '2', limit: '25', search: 'ada' }
source.fromQueryParams(params);Selector subscriptions
Use the utility for value-based subscriptions.
ts
import { subscribeSelector } from '@vielzeug/sourceit';
const stop = subscribeSelector(
source,
(s) => s.meta.pageNumber,
(next, prev) => {
console.log('page changed', prev, '->', next);
},
);
stop();Framework Integration
sourceit exposes framework-agnostic primitives: subscribe(listener), stable current, and stable meta snapshots. Frameworks can consume these directly with their native reactivity APIs.
Local source in UI state
tsx
// React 18+ ships useSyncExternalStore natively.
// For older React, install the 'use-sync-external-store' shim.
import { createLocalSource } from '@vielzeug/sourceit';
import { useMemo, useSyncExternalStore } from 'react';
export function UsersList({ users }: { users: { id: number; name: string }[] }) {
const source = useMemo(() => createLocalSource(users, { limit: 10 }), [users]);
const current = useSyncExternalStore(source.subscribe, () => source.current);
const meta = useSyncExternalStore(source.subscribe, () => source.meta);
return (
<>
<input onChange={(e) => source.search(e.target.value)} placeholder="Search users" />
<ul>
{current.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
<button onClick={() => source.prev()} disabled={meta.isFirstPage}>
Prev
</button>
<button onClick={() => source.next()} disabled={meta.isLastPage}>
Next
</button>
</>
);
}ts
import { createLocalSource } from '@vielzeug/sourceit';
import { onUnmounted, shallowRef } from 'vue';
const source = createLocalSource(users, { limit: 10 });
const state = shallowRef({ current: source.current, meta: source.meta });
const stop = source.subscribe(() => {
state.value = { current: source.current, meta: source.meta };
});
onUnmounted(stop);svelte
<script lang="ts">
import { onDestroy } from 'svelte';
import { createLocalSource } from '@vielzeug/sourceit';
const source = createLocalSource(users, { limit: 10 });
let current = source.current;
let meta = source.meta;
const stop = source.subscribe(() => {
current = source.current;
meta = source.meta;
});
onDestroy(stop);
</script>
<input on:input={(e) => source.search((e.currentTarget as HTMLInputElement).value)} />
{#each current as user}
<div>{user.name}</div>
{/each}
<button on:click={() => source.prev()} disabled={meta.isFirstPage}>Prev</button>
<button on:click={() => source.next()} disabled={meta.isLastPage}>Next</button>Remote source with async lifecycle
tsx
import { createRemoteSource } from '@vielzeug/sourceit';
import { useEffect, useMemo, useSyncExternalStore } from 'react';
export function IssuesList() {
const source = useMemo(
() =>
createRemoteSource({
fetch: ({ limit, page, search }) => api.issues.list({ limit, page, search }),
limit: 20,
}),
[],
);
const meta = useSyncExternalStore(source.subscribe, () => source.meta);
useEffect(() => {
source.refresh();
}, [source]);
return <div>{meta.isLoading ? 'Loading...' : `${meta.totalItems} issues`}</div>;
}ts
import { createRemoteSource } from '@vielzeug/sourceit';
import { onMounted, onUnmounted, shallowRef } from 'vue';
const source = createRemoteSource({
fetch: ({ limit, page, search }) => api.issues.list({ limit, page, search }),
limit: 20,
});
const state = shallowRef({ current: source.current, meta: source.meta });
const stop = source.subscribe(() => {
state.value = { current: source.current, meta: source.meta };
});
onMounted(() => source.refresh());
onUnmounted(stop);svelte
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { createRemoteSource } from '@vielzeug/sourceit';
const source = createRemoteSource({
fetch: ({ limit, page, search }) => api.issues.list({ limit, page, search }),
limit: 20,
});
let meta = source.meta;
const stop = source.subscribe(() => {
meta = source.meta;
});
onMount(() => source.refresh());
onDestroy(stop);
</script>
{#if meta.isLoading}
<p>Loading...</p>
{:else}
<p>{meta.totalItems} issues</p>
{/if}Working with Other Vielzeug Libraries
With Fetchit
Use Fetchit as the transport layer inside createRemoteSource() for consistent retries and error handling.
ts
import { createApi } from '@vielzeug/fetchit';
import { createRemoteSource } from '@vielzeug/sourceit';
const api = createApi({ baseUrl: '/api' });
const source = createRemoteSource({
fetch: ({ page, limit, search }) => api.get('/issues', { query: { page, limit, search } }),
});Best Practices
- Prefer
batch()for grouped changes. - Use
searchNow(term)for explicit submit actions. - Use debounced search for text-input flows.
- Call
ready()only for remote flows that require async completion. - Keep filter/sort payloads serializable for remote URL sync.