Skip to main content

Migrate DataTable from Polaris React

Replace Polaris React DataTable with s-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 instead when merchants need to select resources or run bulk actions.


Anchor to Migrate a data tableMigrate 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

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>
);
}
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,
}}
/>
);
}

Preview

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.


Anchor to Replace the data arraysReplace the data arrays

Polaris ReactPolaris web componentsMigration notes
headingss-table-header-row and s-table-headerRender one header for each column.
rowss-table-body, s-table-row, and s-table-cellMap each record to a row and keep every cell in the same order as its header.
columnContentTypes: 'text'Default header formatText content doesn't need a format value.
columnContentTypes: 'numeric'format="numeric" or format="currency" on the headerThe format controls alignment and table semantics. Format the displayed value in app code.
totalsAn explicit final s-table-rowRender 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.


Anchor to Define the responsive list layoutDefine 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 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.


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.


Anchor to Reconnect loading and paginationReconnect loading and pagination

Polaris ReactPolaris web componentsMigration notes
Adjacent loading stateloading on s-tableSet it for the request that owns the currently displayed table results.
pagination.hasPrevioushasPreviousPageDerive it from the current backend page information.
pagination.hasNexthasNextPageDerive it from the current backend page information.
pagination.onPreviousonPreviousPageRequest the previous cursor without changing the active sort.
pagination.onNextonNextPageRequest 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.


  • 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.

Anchor to Remove Polaris ReactRemove 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.



Was this page helpful?