react-datatable
Guides

Define your table

Use this guide after Define columns, when you are ready to lock the top-level table contract before adding more features.

At this point, assume you already know which columns the table needs and how they access row data.

The goal is to make three decisions up front:

  1. what each row's stable ID is
  2. whether the browser or the server owns the working row set
  3. which top-level datatable props belong in your first integration

Start with the smallest working contract

A table still needs four things to mount correctly:

  • columns
  • getRowId
  • either data or online
  • a normal React render location in your app
<Datatable
  tableKey="customers"
  columns={columns}
  data={customers}
  getRowId={(row) => row.id}
/>

If this contract is not stable yet, do not spend time on grouping, preview, or persistence. Get this boundary right first.

If the columns themselves still feel unsettled, go back and finish Define columns before you freeze the table boundary.

[!NOTE] Screenshot placeholder: the smallest working datatable integration rendered in an app page with only core columns, row identity, and local data wired up.

Pick a real row identity

Give every row a durable domain ID:

getRowId={(row) => row.id}

Use a real business identifier, not an array index.

Stable row IDs keep table behavior predictable when users sort, filter, select, save views, or reload server data.

Choose the top-level data contract

This page is where you decide whether the table is powered by a local row set or an online query contract.

Use data when the browser can own the rows

Choose local mode when:

  • the full result set comfortably fits in the browser
  • filtering and sorting can happen on the client
  • you want the fastest path to a working table
<Datatable
  tableKey="customers"
  columns={columns}
  data={customers}
  getRowId={(row) => row.id}
/>

Use online when the server should stay authoritative

Choose online mode when:

  • filtering, sorting, grouping, or totals must follow server rules
  • the dataset is too large to keep fully in memory
  • you need pagination, infinite scroll, or server-owned row windows
<Datatable
  tableKey="customers"
  columns={columns}
  getRowId={(row) => row.id}
  online={{
    mode: "pagination",
    queryKey: ["customers", workspaceId],
    query: fetchCustomers,
  }}
/>

The visible UI can feel similar in both modes. The real difference is where table work happens.

[!NOTE] Screenshot placeholder: one local table example and one online table example using the same surface, with visual annotations showing browser-owned versus server-owned query behavior.

Keep the app boundary clean

Your app should continue to own:

  • domain types and business rules
  • route params, workspace scope, and permissions
  • server queries, mutations, and side effects
  • surrounding page layout

The datatable should own:

  • row modeling
  • sorting, filtering, grouping, visibility, and selection state
  • rendering the grid and related built-in controls

This split makes later customization safer because you are not mixing business logic into table internals.

Add only the top-level options you already need

A good first integration usually adds a small amount of configuration around the core contract:

<Datatable
  tableKey="customers"
  columns={columns}
  data={customers}
  getRowId={(row) => row.id}
  toolbar={{
    quickSearch: { placeholder: "Search customers..." },
    filterButton: true,
    displayOptions: true,
  }}
  selection={{ enabled: true }}
/>

Start with the options your users will see immediately. Add deeper capabilities one at a time from the guides.

Use choose-a-data-mode to make the next decision more explicit

Once the table contract is mounted, move to Choose a data mode for the deeper decision tree:

  • local versus online ownership
  • pagination versus infinite loading for online mode
  • virtualization as a separate rendering choice

That page is the canonical place to reason about those boundaries. The downstream feature guides should inherit that choice, not reopen it.

Verify the contract before layering on features

Before moving on, check that:

  • rows render with the columns you expect
  • row IDs stay stable across reloads and reordering
  • the chosen data mode matches where your filtering and sorting logic should live
  • the table mounts without extra one-off wiring in feature components
  • the app boundary still feels clean before you add feature-specific options

Next, choose a data mode before you add feature-specific behavior.

On this page