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, andsubscribe(listener), which callslistenerwith each new result and returnsstop. See Queries. - The client has
sync.statusandsync.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:
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);
},
});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:
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:
const stop = sync.onStatus((status) => {
document.title = status.synced ? 'Saved' : 'Saving…';
});Gotchas
- Read with
get()first, then subscribe.subscribecalls 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.
Your own auth
Ferry has no users of its own. The server asks one question of every request, auth(request), and your code answers it: a user id, or null for a 401. Whatever already signs your users in works.
Next.js
The browser keeps the data, and one route handler of the same Next.js app is the server. It uses the schema of the Quick start.