Migrate Data Table 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
Polaris web components
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
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 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.
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:
primaryidentifies the row, such as the product title.secondaryplaces supporting content in the secondary area.inlinekeeps short supporting content on the primary line.labeleddisplays 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.
Anchor to Rebuild sortingRebuild 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.
Anchor to Reconnect loading and paginationReconnect 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.
Anchor to Test the migrationTest 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.
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.