---
title: "TanStack Query"
description: "The query entry: QueryView draws a query in the dashboard’s states, a skeleton or spinner before the first answer, an error with a retry, an empty state, and the data, dimmed while the next page loads. Each example has its own QueryClient and a pretend server."
---

> Documentation Index
> Fetch the complete documentation index at: https://epidermis.getarbol.com/llms.txt
> Use this file to discover all available pages before exploring further.

# TanStack Query

## A table, page by page

The next page keeps the current rows on screen, dimmed, until it arrives.

```tsx
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.

```tsx
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

```tsx
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`](https://github.com/getarbol/project-arbol-ui/blob/main/src/query/query-view.examples.tsx), which the library's tests render too.

Source: https://epidermis.getarbol.com/components/data/query-view/index.mdx
