Skip to content

Basic Usage

Create a page source and call reload() to start the first request. Construction performs no I/O.

ts
import { createPageSource } from '@vielzeug/sourcerer';

const names = ['Ada', 'Grace', 'Linus'];
const source = createPageSource({
  load: async ({ page, pageSize }) => {
    const start = (page - 1) * pageSize;
    return { items: names.slice(start, start + pageSize), totalItems: names.length };
  },
  pageSize: 2,
});

await source.reload();
console.log(source.state.items);
source.dispose();

Read one state object per render. It contains items, loading, error, params, and pagination metadata from one committed result. Commands return Promise<void>; read source.state after awaiting them.

Pass Loader Parameters

Use params for application-owned filters, search terms, sort state, or request options. setParams() replaces the entire value and resets pagination.

ts
import { createPageSource } from '@vielzeug/sourcerer';

type UserParams = { role: 'admin' | 'user'; search: string };
const users = ['Ada', 'Grace', 'Linus'];
const source = createPageSource<string, UserParams>({
  load: async ({ page, pageSize, params }) => {
    const matching = users.filter((name) => name.toLowerCase().includes(params.search.toLowerCase()));
    const start = (page - 1) * pageSize;
    return { items: matching.slice(start, start + pageSize), totalItems: matching.length };
  },
  params: { role: 'admin', search: '' },
});

await source.setParams({ role: 'admin', search: 'ada' });
console.log(source.state.params);
source.dispose();

A non-optional parameter type requires an initial params value. Sourcerer compares parameter values with Object.is; reuse a value when setting it again should be a no-op.

While replacement parameters load, state.params continues to describe the committed items and state.pendingParams contains the requested value. Page-only navigation does not set pendingParams.

Use direct commands for numbered-page navigation. setPageSize() resets to page one.

ts
await source.reload();
await source.next();
await source.goTo(4);
await source.previous();
await source.first();
await source.last();
await source.setPageSize(50);

After the first successful load, navigation is clamped to the known pageCount. Before that load, goTo() accepts any positive page because the total is unknown.

Filter Local Collections

Use a local source when data is already in memory. Its commands are synchronous.

ts
import { createLocalSource } from '@vielzeug/sourcerer';

const source = createLocalSource(['Ada', 'Grace', 'Linus'], {
  filter: (name, search: string) => name.toLowerCase().includes(search.toLowerCase()),
  pageSize: 2,
  params: '',
});

source.setParams('a');
source.next();
source.setItems(['Ada', 'Grace']);
console.log(source.state.items);
source.dispose();

Use ordinary arrays for one-off transformations. A local source is useful when pagination, subscriptions, and lifecycle ownership are also required.

Use Cursor Pagination

Return opaque cursors from the loader. next() and previous() use only cursors from the committed state.

ts
import { createCursorSource } from '@vielzeug/sourcerer';

const rows = ['A', 'B', 'C', 'D'];
const source = createCursorSource({
  load: async ({ after, pageSize }) => {
    const start = after ? Number(after) : 0;
    const items = rows.slice(start, start + pageSize);
    const next = start + items.length;
    return {
      items,
      nextCursor: next < rows.length ? String(next) : undefined,
      previousCursor: start > 0 ? String(Math.max(0, start - pageSize)) : undefined,
    };
  },
  pageSize: 2,
});

await source.reload();
await source.next();
console.log(source.state.items);
source.dispose();

Changing params or page size clears current cursor direction. A source cannot start with both after and before.

Build an Infinite Feed

Use an infinite source when later pages should append to committed items.

ts
import { createInfiniteSource } from '@vielzeug/sourcerer';

const values = [1, 2, 3, 4, 5];
const source = createInfiniteSource({
  load: async ({ page, pageSize }) => {
    const start = (page - 1) * pageSize;
    return { items: values.slice(start, start + pageSize), totalItems: values.length };
  },
  pageSize: 2,
});

await source.reload();
await source.loadMore();
console.log(source.state.items);
source.dispose();

loadMore() is a no-op while another request is active or after pagination.hasMore becomes false. setParams() and setPageSize() replace the feed from page one.

Handle Requests and Errors

A new remote command aborts older work. Superseded or disposed work settles without committing. A current loader failure preserves committed items, writes the error to state, and rejects the command.

ts
try {
  await source.reload();
} catch (error) {
  console.error(error);
  console.log(source.state.error);
}

Loader result arrays are copied before publication. Commands after disposal throw or reject SourcererDisposedError.

Testing

Inject deterministic loaders and await commands before asserting final state.

ts
import { expect, it } from 'vitest';
import { createPageSource } from '@vielzeug/sourcerer';

it('loads a page', async () => {
  const source = createPageSource({ load: async () => ({ items: ['Ada'], totalItems: 1 }) });

  await source.reload();

  expect(source.state.items).toEqual(['Ada']);
  source.dispose();
});

Framework Integration

Create one source per component lifetime and bridge subscribe() to the framework lifecycle.

tsx
import { createPageSource } from '@vielzeug/sourcerer';
import { useEffect, useMemo, useSyncExternalStore } from 'react';

export function Users() {
  const source = useMemo(
    () => createPageSource({ load: async () => ({ items: [{ id: 1, name: 'Ada' }], totalItems: 1 }) }),
    [],
  );
  const state = useSyncExternalStore(source.subscribe, () => source.state);

  useEffect(() => {
    void source.reload().catch(() => undefined);
    return () => source.dispose();
  }, [source]);

  return <p>{state.loading ? 'Loading' : state.items.length}</p>;
}
ts
import { onUnmounted, shallowRef } from 'vue';
import { createPageSource } from '@vielzeug/sourcerer';

const source = createPageSource({ load: async () => ({ items: [{ id: 1, name: 'Ada' }], totalItems: 1 }) });
const state = shallowRef(source.state);
const stop = source.subscribe((next) => (state.value = next));
void source.reload().catch(() => undefined);

onUnmounted(() => {
  stop();
  source.dispose();
});
svelte
<script lang="ts">
  import { onDestroy } from 'svelte';
  import { createPageSource } from '@vielzeug/sourcerer';

  const source = createPageSource({ load: async () => ({ items: [{ id: 1, name: 'Ada' }], totalItems: 1 }) });
  let state = source.state;
  const stop = source.subscribe((next) => (state = next));
  void source.reload().catch(() => undefined);

  onDestroy(() => {
    stop();
    source.dispose();
  });
</script>

{#if state.loading}Loading{/if}
{#each state.items as user}{user.name}{/each}

Working with Other Vielzeug Libraries

Use Courier for transport policy and pass the source-owned cancellation signal through.

ts
import { createCourier } from '@vielzeug/courier';
import { createPageSource } from '@vielzeug/sourcerer';

type User = { id: number; name: string };
type Response = { data: User[]; total: number };
const courier = createCourier({ baseUrl: '/api' });
const source = createPageSource({
  load: async ({ page, pageSize, params: search, signal }) => {
    const result = await courier.get<Response>('/users', { query: { page, pageSize, search }, signal });
    return { items: result.data, totalItems: result.total };
  },
  params: '',
});

Use Scout to compute indexed local matches, Ripple to project state into reactive computations, and Wayfinder to validate and synchronize params with the URL.

Best Practices

  • Dispose each source with its owning view, request, or service scope.
  • Read one state object per render.
  • Await direct commands or handle rejected current-request failures.
  • Keep transport, caching, and persistence policy outside Sourcerer.
  • Treat params as an immutable value and replace it atomically.
  • Debounce text input before calling setParams() on a remote source.
  • Validate route and form values before constructing params or page numbers.
  • Keep rendering committed items while loading is true.