Ferry
Concepts

useQuery or a state library?

Ferry's data is on the device, so any UI can read it. There are four ways to do that, and they are not rivals: they read the same local database. The difference is where your app already keeps its state.

                       sync.tasks.where(...)
                              |
        +---------------+-----+---------+------------------+
        |               |               |                  |
    useQuery()    @ferrysync/zustand  @ferrysync/jotai  @ferrysync/legend-state
    (React hook)  (a Zustand store)   (Jotai atoms)     (Legend-State observables)

Which one to pick

Your app…UseWhy
is new, or uses React with no global state libraryuseQueryNothing to install. Reads exactly what a component needs and re-renders only when it changes
already reads its state through Zustand selectors@ferrysync/zustandThe tables join the store you have, next to your own state
already builds on Jotai atoms@ferrysync/jotaiThe tables and the status are atoms, so derived atoms can use them
already uses Legend-State observables@ferrysync/legend-stateYou bind tables into observables you own
is not React (Vue, Svelte, Solid, vanilla)get and subscribeTwo methods on every query are all a binding needs

When unsure, start with useQuery. Moving to an adapter later changes how a component reads, not the data or the writes.

What does not change

Whichever you pick:

  • Writes are the same. Always call the client: sync.tasks.update(id, …). The adapters are read-only mirrors, so never write to them with setState, set or an observable's set.
  • There is no loading state per read. The data is already on the device. sync.ready says once when local data is open: SyncProvider waits for it, the Zustand and Jotai adapters expose it as ready, and Legend-State bindings fill in as the data loads.
  • Sync status is sync.status. useSyncStatus() reads it in React, and the Zustand and Jotai adapters mirror it as status.

What does change

useQueryZustand, Jotai, Legend-State
You describeA query: filters, order, limitWhole tables, mirrored by id
Filtering and sortingIn the queryIn your selector, derived atom or computed observable
FitsScreens that show a slice of the dataApps that already pass state around through one store

Using both

Nothing stops an app from using useQuery on most screens and an adapter where a store already exists: they share the one client, so they always agree.

Client
import { useQuery } from 'ferrysync/react';

import { sync } from './sync';

const OpenCount = () => {
  const open = useQuery(sync.tasks.where({ done: false }));
  return <p>{open.length} open</p>;
};

Next

On this page