A table, page by page
The next page keeps the current rows on screen, dimmed, until it arrives.
import { QueryClient, QueryClientProvider, keepPreviousData, useQuery } from '@tanstack/react-query';
import type { ColumnDef } from '@tanstack/react-table';
import { useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import { DataTable, Pagination, TableSkeleton } from '@arbol-ai/ui';
import type { DataTableFeatures } from '@arbol-ai/ui';
import { QueryView } from '@arbol-ai/ui/query';
interface Contact {
id: string;
name: string;
channel: string;
}
const CONTACTS: Contact[] = [
{ id: 'c1', name: 'Ana Torres', channel: 'Phone' },
{ id: 'c2', name: 'Luis Gómez', channel: 'WhatsApp' },
{ id: 'c3', name: 'Marta Ruiz', channel: 'Web chat' },
{ id: 'c4', name: 'Jorge Peña', channel: 'WhatsApp' },
{ id: 'c5', name: 'Sofía Díaz', channel: 'Phone' },
{ id: 'c6', name: 'Pedro Lara', channel: 'Web chat' },
];
const PAGE_SIZE = 3;
const COLUMNS: ColumnDef<DataTableFeatures, Contact>[] = [
{ accessorKey: 'name', header: 'Name', meta: { fill: true } },
{ accessorKey: 'channel', header: 'Channel', size: 160 },
];
const answerLater = async <T,>(answer: () => T) => {
// oxlint-disable-next-line promise/avoid-new -- a timer is the pretend server's latency
await new Promise((resolve) => {
setTimeout(resolve, 800);
});
return answer();
};
const ExampleClient = ({ children }: { children: ReactNode }) => {
const client = useMemo(() => new QueryClient({ defaultOptions: { queries: { retry: false } } }), []);
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
};
const ContactsTable = () => {
const [page, setPage] = useState(1);
const contacts = useQuery({
placeholderData: keepPreviousData,
queryFn: async () => await answerLater(() => CONTACTS.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)),
queryKey: ['contacts', page],
});
return (
<div className="grid gap-3">
<QueryView
pending={<TableSkeleton columns={[{ width: 'w-32' }, { width: 'w-20' }]} label="Loading contacts" />}
query={contacts}
>
{(rows, { isUpdating }) => (
<DataTable aria-label="Contacts" columns={COLUMNS} data={rows} isUpdating={isUpdating} />
)}
</QueryView>
<Pagination onPage={setPage} page={page} pageSize={PAGE_SIZE} total={CONTACTS.length} />
</div>
);
};
<ExampleClient>
<ContactsTable />
</ExampleClient>An error with a retry
The first request fails; the retry stays pending until the second one answers.
Loading…
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react';
import type { ReactNode } from 'react';
import { QueryView } from '@arbol-ai/ui/query';
const answerLater = async <T,>(answer: () => T) => {
// oxlint-disable-next-line promise/avoid-new -- a timer is the pretend server's latency
await new Promise((resolve) => {
setTimeout(resolve, 800);
});
return answer();
};
const ExampleClient = ({ children }: { children: ReactNode }) => {
const client = useMemo(() => new QueryClient({ defaultOptions: { queries: { retry: false } } }), []);
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
};
const FlakyCampaigns = () => {
const requests = useRef(0);
const campaigns = useQuery({
queryFn: async () =>
await answerLater(() => {
requests.current += 1;
if (requests.current === 1) {
throw new Error('Service unavailable');
}
return ['Flu shots reminder', 'Missed appointments'];
}),
queryKey: ['campaigns'],
});
return (
<QueryView
errorDescription="We couldn't load the campaigns. Check your connection and try again."
query={campaigns}
>
{(names) => (
<ul aria-label="Campaigns" className="grid gap-1 text-sm">
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
)}
</QueryView>
);
};
<ExampleClient>
<FlakyCampaigns />
</ExampleClient>An empty answer
Loading…
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import type { ReactNode } from 'react';
import { EmptyState } from '@arbol-ai/ui';
import { QueryView } from '@arbol-ai/ui/query';
const answerLater = async <T,>(answer: () => T) => {
// oxlint-disable-next-line promise/avoid-new -- a timer is the pretend server's latency
await new Promise((resolve) => {
setTimeout(resolve, 800);
});
return answer();
};
const ExampleClient = ({ children }: { children: ReactNode }) => {
const client = useMemo(() => new QueryClient({ defaultOptions: { queries: { retry: false } } }), []);
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
};
const NoAppointments = () => {
const appointments = useQuery({
queryFn: async () => await answerLater((): string[] => []),
queryKey: ['appointments'],
});
return (
<QueryView
empty={
<EmptyState description="Bookings from every channel show up here." title="No appointments today" />
}
query={appointments}
>
{(names) => (
<ul aria-label="Appointments">
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
)}
</QueryView>
);
};
<ExampleClient>
<NoAppointments />
</ExampleClient>Source
The examples above are src/query/query-view.examples.tsx, which the library’s tests render too.