Ferry
Examples

Your own bindings

The React hooks, the Zustand store, the Jotai atoms and the Legend-State observables are thin layers over two things every client has, so any framework or state library can have its own in a few lines:

  • A query has get(), its current result, and subscribe(listener), which calls listener with each new result and returns stop. See Queries.
  • The client has sync.status and sync.onStatus(listener), the same pair for the sync state.

Results stay the same object until they change, so a binding can compare by identity and never copies.

Svelte

A Svelte store is anything with a subscribe that hands over the current value first. A query is one line away:

live.tsClient
import type { LiveQuery } from 'ferrysync';

/** A Svelte readable store over a query: `$tasks` in a component. */
export const live = <Result>(query: LiveQuery<Result>) => ({
  subscribe: (run: (result: Result) => void) => {
    run(query.get());
    return query.subscribe(run);
  },
});
tasks.tsClient
import { live } from './live';
import { sync } from './sync';

export const openTasks = live(sync.tasks.where({ done: false }).orderBy('title'));

Vue

A composable keeps the result in a shallow ref, and stops listening with the component's scope:

use-live-query.tsClient
import type { LiveQuery } from 'ferrysync';
import { onScopeDispose, shallowRef } from 'vue';

/** The query's result as a ref, current until the component unmounts. */
export const useLiveQuery = <Result>(query: LiveQuery<Result>) => {
  const result = shallowRef(query.get());
  const stop = query.subscribe((next) => {
    result.value = next;
  });
  onScopeDispose(stop);
  return result;
};

The sync state

The same shape works for the status, for a "Saving…" indicator in any framework:

Client
const stop = sync.onStatus((status) => {
  document.title = status.synced ? 'Saved' : 'Saving…';
});

Gotchas

  • Read with get() first, then subscribe. subscribe calls the listener with new results only, not the current one.
  • Always call stop. A forgotten subscription keeps its query, and its listener, alive.
  • Queries made of filters, orders and limits are interned: writing the same chain twice gives the same query, so a binding can take one inline. A predicate query is new on every call.

On this page