Legend-State
@ferrysync/legend-state mirrors the client's tables into
Legend-State observables. Use it when the app already
reads its state through observables, with fine-grained re-renders.
import { observable } from '@legendapp/state';
import { bindLegendState, bindTable } from '@ferrysync/legend-state';
import type { Project, Task } from './schema';
import { sync } from './sync';
export const store$ = observable<{
projects: Record<string, Project>;
tasks: Record<string, Task>;
}>({ projects: {}, tasks: {} });
bindLegendState(sync, [
bindTable(sync.projects, store$.projects),
bindTable(sync.tasks, store$.tasks),
]);const TaskTitle = observer(({ id }: { id: string }) => {
const title = useValue(() => store$.tasks[id]?.title.get());
return <p>{title}</p>;
});bun add @ferrysync/legend-state @legendapp/state@legendapp/state (3, from 3.0.0-beta.48) and ferrysync are peer dependencies.
Bindings
| Function | What it does |
|---|---|
bindTable(table, target$) | Mirrors every row of table into target$, an observable of rows by id |
bindRow(table, target$, id) | Mirrors the row id into target$, an observable of one row or null |
bindLegendState(sync, bindings) | Fills every binding, follows the client's changes, and returns the function to stop |
bindRow mirrors one row: the project a screen shows, or a settings row with a known id.
const current$ = observable<Project | null>(null);
const stop = bindLegendState(sync, [bindRow(sync.projects, current$, projectId)]);How it behaves
- One change, one batch. Each change of the client is applied in one Legend-State
batch(), so observers run once, however many tables it touched. Status changes write nothing. - Only the rows that changed are written (
target$[id].set(row),target$[id].delete()). The record and the other rows keep their objects, and only those rows' observers hear of it. - It survives
sync.reset(). It reads the client's tables, not its engine: at a sign-out the observables empty with the tables, then fill with the new data. - Types are checked. An observable of another row type, or of a wider one, is refused.
Writing
The observables are a read-only mirror. Write through the client, never with set: the next change
would overwrite it, and nothing would be stored or synced.
sync.tasks.update('t1', { done: true }); // store$.tasks.t1 updates before this returnsThe engine form
Apps built on the low-level engine (createSyncEngine) bind models instead of tables, with
bindEngine(engine, [bindModel(model, target$), bindSingleton(model, target$, id)], { onInvalidRow }).
It follows one engine, so it does not survive sync.reset(), which replaces sync.engine: bind the
client with bindLegendState whenever there is one.
Gotchas
- Bind once, next to the store, for the app's lifetime. Each
bindLegendStatecall subscribes again; call the function it returns to stop one. - Bind before reading. The binding fills the observables when it is made; a binding made before
sync.readyfills them as the data loads.