Ferry
Examples

TanStack Query

Ferry and TanStack Query in one app, each with its own job:

  • Ferry holds the rows the app creates, edits and syncs. They are read locally, with no loading state.
  • TanStack Query fetches what lives only on a server: another API, or numbers the server computes from the synced rows.

It uses the schema and client of the Quick start.

The provider

Both providers wrap the app; their order does not matter.

app.tsxClient
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SyncProvider } from 'ferrysync/react';

import { sync } from './sync';

const queryClient = new QueryClient();

export const App = ({ children }: { children: React.ReactNode }) => (
  <QueryClientProvider client={queryClient}>
    <SyncProvider sync={sync} fallback={<p>Opening…</p>}>
      {children}
    </SyncProvider>
  </QueryClientProvider>
);

Local rows next to fetched data

Both libraries call their hook useQuery. Import Ferry's under its other name, useLiveQuery:

trip.tsxClient
import { useQuery } from '@tanstack/react-query';
import { useLiveQuery } from 'ferrysync/react';

import { sync } from './sync';

export const Trip = ({ projectId }: { projectId: string }) => {
  // Local: there on the first render, offline too.
  const tasks = useLiveQuery(sync.tasks.where({ projectId, done: false }));
  // Remote: fetched, cached, and pending until it answers.
  const weather = useQuery({ queryKey: ['weather', projectId], queryFn: () => fetchWeather() });

  return (
    <section>
      <p>{weather.isPending ? 'Checking the weather…' : weather.data}</p>
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>{task.title}</li>
        ))}
      </ul>
    </section>
  );
};

Data the server computes from synced rows

A report the server builds from the tasks changes when the tasks reach the server. Put lastSyncId in the query key: it grows only when the device catches up with the server's log, so the report refetches when the server may have news, and not on every render or every local write.

report.tsxClient
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import { useSyncStatus } from 'ferrysync/react';

import { sync } from './sync';

export const Report = ({ projectId }: { projectId: string }) => {
  const { lastSyncId } = useSyncStatus(sync);
  const report = useQuery({
    queryKey: ['report', projectId, lastSyncId],
    queryFn: () => fetchReport(projectId),
    // Keep showing the last report while the next one loads.
    placeholderData: keepPreviousData,
  });

  if (report.data === undefined) {
    return <p>Loading the report…</p>;
  }
  return (
    <p>
      {report.data.done} done, {report.data.open} open
    </p>
  );
};

Gotchas

  • Do not wrap Ferry's reads in useQuery from TanStack. It adds a second cache that every write and every change from another device would have to invalidate, and a loading state for data that is already in memory.
  • Do not write through useMutation. sync.tasks.update(…) applies at once and waits in the outbox when offline; a mutation would add a pending state to something that never waits.
  • Offline, TanStack's queries pause; Ferry's do not. That is expected: the report needs the server, the tasks do not.

On this page