> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sublay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Use table

> Read and mutate a custom table's rows with pagination, sorting, filtering, and soft-delete

## Overview

`useTable` is the React/React Native hook for a custom table's rows, backed by RTK Query against the `/db` surface. It returns the current page of rows plus loading/refetch state, query controls, and row CRUD actions. The query knobs (page, limit, sort, filters, `includeDeleted`) are stored in the `tables` Redux slice keyed by table name, so multiple consumers of the same table share one view.

See [Custom Tables — Overview](/v8/sdk/tables/overview) for a guide, and [Custom Tables](/v8/custom-tables) for the feature concept.

## Usage Example

```tsx theme={null}
import { useTable } from "@sublay/react-js";

type EventRow = {
  id: string;
  name: string;
  capacity: number;
  createdAt: string;
};

function Events() {
  const {
    rows,
    pagination,
    loading,
    view,
    setPage,
    setSort,
    setFilters,
    createRow,
    updateRow,
    deleteRow,
  } = useTable<EventRow>("Events");

  return (
    <>
      {loading && <Spinner />}
      {rows.map((e) => (
        <Row key={e.id} event={e} onSave={(d) => updateRow(e.id, d)} onDelete={() => deleteRow(e.id)} />
      ))}
      {/* Both halves come off `view.page` (what you asked for) rather than
          `pagination` (what is on screen) — see the note below. `totalPages`
          is safe to read because it describes the whole result set, not the
          page. */}
      {pagination && view.page < pagination.totalPages && (
        <button onClick={() => setPage(view.page + 1)}>Next</button>
      )}
    </>
  );
}
```

## Parameters

<ParamField path="tableName" type="string" required>
  The **logical** custom-table name (without the `custom_` prefix). It may change between renders: the hook never shows the previous table's rows under the new name. It loads the new table from scratch, or shows that table's own rows straight away if they are still cached.
</ParamField>

<ParamField path="options" type="Partial<TableViewState>">
  Optional initial view — any of `page`, `limit`, `sortBy`, `sortDir`, `filters`, `includeDeleted`. Used to seed the stored view the first time this table is read.
</ParamField>

## Return Values

<ResponseField name="rows" type="T[]">
  The current page of rows.
</ResponseField>

<ResponseField name="pagination" type="PaginationMetadata | null">
  Pagination metadata (`page`, `pageSize`, `totalPages`, `totalItems`, `hasMore`), or `null` before the first load.

  It describes the page **currently on screen**, which during a page change is still the previous one — the rows and their metadata stay in step.

  So a pager needs its *target* and its *bound* from `view.page`, the page you last asked for, not from `pagination`:

  * `setPage(pagination.page + 1)` computes the same target twice if clicked again before the first request lands, so the second click appears to do nothing.
  * `pagination.hasMore` is likewise the previous page's answer, so a second click can run off the end — asking for page 3 when page 2 was the last.

  `totalPages` and `totalItems` are safe to read mid-change: they describe the whole result set rather than a page, so `view.page < pagination.totalPages` is a correct bound either way.
</ResponseField>

<ResponseField name="loading" type="boolean">
  `true` when there is no page to show and a request is in flight — the first load, and a `tableName` change. An empty table is a page, so it reports `false` with zero rows.

  Changing page, sort or filters keeps the previous rows on screen with `loading: false` while the new query runs, so the table does not blank out between pages. Changing `tableName` does blank it: another table's rows under a new table's name would be wrong data, not stale data. Returning to a table this hook has already loaded shows that table's own rows again rather than blanking, for as long as they remain cached.

  [`useTableRow`](/v8/hooks/tables/use-table-row) follows the same rule for the same reason — a `rowId` change reports `loading: true` rather than leaving the previous record on screen.
</ResponseField>

<ResponseField name="error" type="unknown">
  The query error, if any.
</ResponseField>

<ResponseField name="refetch" type="() => void">
  Re-runs the current query.
</ResponseField>

<ResponseField name="view" type="TableViewState">
  The current stored view: `{ page, limit, sortBy, sortDir, filters, includeDeleted }`.
</ResponseField>

<ResponseField name="setView" type="(view: Partial<TableViewState>) => void">
  Merge-updates the stored view.
</ResponseField>

<ResponseField name="setPage" type="(page: number) => void">
  Changes the current page.
</ResponseField>

<ResponseField name="setFilters" type="(filters: DbFilter[]) => void">
  Replaces the filter clauses (and resets to page 1). Each clause is `{ column, operator, value }`; operators are `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `contains`, `like`, `isNull`, AND-combined.
</ResponseField>

<ResponseField name="setSort" type="(sortBy?: string, sortDir?: 'asc' | 'desc') => void">
  Sets the sort column/direction (and resets to page 1).
</ResponseField>

<ResponseField name="setIncludeDeleted" type="(includeDeleted: boolean) => void">
  On a paranoid table, toggles whether soft-deleted rows are included (resets to page 1).
</ResponseField>

<ResponseField name="createRow" type="(data: Record<string, unknown>) => Promise<T>">
  Inserts a row. Managed columns (`id`/`createdAt`/`updatedAt`/`deletedAt`) are server-set and rejected from the body.
</ResponseField>

<ResponseField name="updateRow" type="(rowId: string, data: Record<string, unknown>) => Promise<T>">
  Updates a row; `updatedAt` is bumped automatically on a timestamped table.
</ResponseField>

<ResponseField name="deleteRow" type="(rowId: string, opts?: { force?: boolean }) => Promise<{ deleted: boolean; soft: boolean }>">
  Deletes a row. Soft-deletes on a paranoid table by default; pass `{ force: true }` to hard-delete.
</ResponseField>

<ResponseField name="restoreRow" type="(rowId: string) => Promise<T>">
  Clears `deletedAt` on a soft-deleted row (paranoid tables only).
</ResponseField>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.