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.
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:
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.
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
useQueryfrom 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.