---
title: Migrate DataTable from Polaris React
description: >-
  Replace the Polaris React DataTable component with an s-table composition for
  responsive tabular data, totals, sorting, loading, and pagination.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/data-table
  md: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/data-table.md
api_name: app-home
---

# Migrate Data​Table from Polaris React

Replace Polaris React `DataTable` with [`s-table`](https://shopify.dev/docs/api/app-home/web-components/layout-and-structure/table). Instead of passing headings and rows as arrays, render explicit table elements and keep one source of truth for sort and pagination values and the rows they load.

Use the [IndexTable migration](https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/index-table) instead when merchants need to select resources or run bulk actions.

***

## Migrate a data table

The following example migrates a sales table with numeric columns, a total row, backend sorting, loading, and cursor pagination.

## Migrating a product sales table

##### Polaris web components

```tsx
import {useSearchParams} from 'react-router';

function ProductSalesTable({products, totals, pageInfo, currency, locale}) {
  const [searchParams, setSearchParams] = useSearchParams();
  const sort = searchParams.get('sort') ?? 'revenue-desc';
  const moneyFormatter = new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
  });

  function updatePage(patch: Record<string, string>) {
    const next = new URLSearchParams(searchParams);

    for (const [name, value] of Object.entries(patch)) {
      next.set(name, value);
    }

    setSearchParams(next);
  }

  function updateSort(value: string) {
    const next = new URLSearchParams(searchParams);
    next.set('sort', value);
    next.delete('cursor');
    next.delete('direction');
    setSearchParams(next, {replace: true});
  }

  return (
    <s-section padding="none" accessibilityLabel="Product sales">
      <s-table
        paginate
        loading={pageInfo.loading}
        hasPreviousPage={pageInfo.hasPreviousPage}
        hasNextPage={pageInfo.hasNextPage}
        onPreviousPage={() =>
          updatePage({
            cursor: pageInfo.startCursor,
            direction: 'previous',
          })
        }
        onNextPage={() =>
          updatePage({cursor: pageInfo.endCursor, direction: 'next'})
        }
      >
        <s-query-container slot="filters">
          <s-select
            label="Sort by"
            value={sort}
            onChange={(event) => updateSort(event.currentTarget.value)}
          >
            <s-option value="revenue-desc">Revenue, high to low</s-option>
            <s-option value="revenue-asc">Revenue, low to high</s-option>
            <s-option value="units-desc">Units sold, high to low</s-option>
            <s-option value="units-asc">Units sold, low to high</s-option>
          </s-select>
        </s-query-container>

        <s-table-header-row>
          <s-table-header listSlot="primary">Product</s-table-header>
          <s-table-header listSlot="labeled" format="numeric">
            Units sold
          </s-table-header>
          <s-table-header listSlot="labeled" format="currency">
            Revenue
          </s-table-header>
        </s-table-header-row>

        <s-table-body>
          {products.map((product) => (
            <s-table-row key={product.id}>
              <s-table-cell>{product.title}</s-table-cell>
              <s-table-cell>{product.unitsSold}</s-table-cell>
              <s-table-cell>
                {moneyFormatter.format(product.revenue)}
              </s-table-cell>
            </s-table-row>
          ))}
          <s-table-row>
            <s-table-cell>
              <s-text type="strong">Total</s-text>
            </s-table-cell>
            <s-table-cell>
              <s-text type="strong">{totals.unitsSold}</s-text>
            </s-table-cell>
            <s-table-cell>
              <s-text type="strong">
                {moneyFormatter.format(totals.revenue)}
              </s-text>
            </s-table-cell>
          </s-table-row>
        </s-table-body>
      </s-table>
    </s-section>
  );
}
```

##### Polaris React

```tsx
import {DataTable} from '@shopify/polaris';

export function ProductSalesTable({products, totals, pageInfo, changeSort}) {
  return (
    <DataTable
      columnContentTypes={['text', 'numeric', 'numeric']}
      headings={['Product', 'Units sold', 'Revenue']}
      rows={products.map((product) => [
        product.title,
        product.unitsSold,
        product.revenue,
      ])}
      totals={['Total', totals.unitsSold, totals.revenue]}
      sortable={[false, true, true]}
      defaultSortDirection="descending"
      initialSortColumnIndex={2}
      onSort={(columnIndex, direction) =>
        changeSort({columnIndex, direction})
      }
      pagination={{
        hasPrevious: pageInfo.hasPreviousPage,
        hasNext: pageInfo.hasNextPage,
        onPrevious: pageInfo.loadPrevious,
        onNext: pageInfo.loadNext,
      }}
    />
  );
}
```

This example assumes a React Router data route. Its loader reads `sort`, `cursor`, and `direction` from the request URL and returns `{products, totals, pageInfo}`. Keeping those values in the URL and using server cursor pagination are choices made by this example, not requirements of `s-table`.

***

## Replace the data arrays

| Polaris React | Polaris web components | Migration notes |
| - | - | - |
| `headings` | `s-table-header-row` and `s-table-header` | Render one header for each column. |
| `rows` | `s-table-body`, `s-table-row`, and `s-table-cell` | Map each record to a row and keep every cell in the same order as its header. |
| `columnContentTypes: 'text'` | Default header format | Text content doesn't need a `format` value. |
| `columnContentTypes: 'numeric'` | `format="numeric"` or `format="currency"` on the header | The format controls alignment and table semantics. Format the displayed value in app code. |
| `totals` | An explicit final `s-table-row` | Render labels and values in the corresponding cells. Use strong text to distinguish the row. |

Keep the totals row in the same column order as the body. In the example, the label is in the product column, followed by total units and formatted total revenue.

`format="currency"` doesn't localize or add a currency symbol. Format money with the merchant's locale and currency before rendering it. Keep numeric values as numbers in your data model so sorting and totals don't operate on formatted strings.

***

## Define the responsive list layout

At a narrow app viewport, `s-table` presents rows as a list. A narrow container inside a wide viewport remains a table. Set [`listSlot`](https://shopify.dev/docs/api/app-home/web-components/layout-and-structure/table#tableheader-propertydetail-listslot) on a header to control where that column's cell value appears in each list item:

* `primary` identifies the row, such as the product title.
* `secondary` places supporting content in the secondary area.
* `inline` keeps short supporting content on the primary line.
* `labeled` displays the header label beside the value, as for units sold and revenue.

Omitting `listSlot` doesn't hide a column; its value uses the default `labeled` placement.

Don't use the old `truncate` behavior as a default. Shorten the displayed content intentionally, move supporting data to an appropriate list slot, and preserve access to any value that merchants need.

***

## Rebuild sorting

`s-table` doesn't sort rows or own a sort direction. Replace `sortable`, `defaultSortDirection`, `initialSortColumnIndex`, and `onSort` with an explicit sort control and app state.

The table header row isn't rendered in the narrow list layout, so don't put the only sort controls in sortable headers. The example uses a labeled `s-select` in the `filters` slot, which remains available in both layouts. Changing it writes a stable value such as `revenue-desc` to URL search parameters. The route uses that value for its backend query, removes cursors from the previous result order, and reloads the table.

If sorting happens locally, sort a copy of the source records rather than mutating the array passed to the component. Use the same sort value to derive both the selected option and the displayed rows.

***

## Reconnect loading and pagination

| Polaris React | Polaris web components | Migration notes |
| - | - | - |
| Adjacent loading state | `loading` on `s-table` | Set it for the request that owns the currently displayed table results. |
| `pagination.hasPrevious` | `hasPreviousPage` | Derive it from the current backend page information. |
| `pagination.hasNext` | `hasNextPage` | Derive it from the current backend page information. |
| `pagination.onPrevious` | `onPreviousPage` | Request the previous cursor without changing the active sort. |
| `pagination.onNext` | `onNextPage` | Request the next cursor without changing the active sort. |

Add `paginate` when the table has pagination controls. Reset its cursor whenever sorting, filters, or search changes; a cursor from the old query doesn't identify a page in the new result set.

Render empty and error feedback outside the row mapping. An empty request should explain why there are no rows and offer the relevant recovery action. A failed request should remain visible and retryable rather than leaving a stale table in place.

***

## Test the migration

* Compare every heading, cell, total, and formatted value with the existing table.
* Change every sort option, then reload and use browser navigation to verify query state.
* Navigate forward and backward and confirm that sort and filters remain applied.
* Test loading, empty, failed, first-page, and last-page states.
* Test wide and narrow app viewports, including long product names and localized numbers.
* Confirm that sorting works in the narrow list layout.
* Verify header relationships, reading order, and pagination controls with a keyboard and screen reader.

***

## Remove Polaris React

After every `DataTable` call site is migrated, remove row-array builders, Polaris-only sort adapters, and the `DataTable` import. Remove `@shopify/polaris` only after no other route in scope imports it.

***

## Related guidance

* [Table component](https://shopify.dev/docs/api/app-home/web-components/layout-and-structure/table)
* [Migrate IndexTable from Polaris React](https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/index-table)
* [Migrate Filters from Polaris React](https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/filters)

***
