Migrate Select All Actions from Polaris React
Polaris web components don't have a standalone SelectAllActions component. Compose select-all behavior into the index table or resource list that owns the selected IDs and bulk operations.
Define what all means before migrating: all selectable resources on the loaded page, all loaded resources, or every result matching the current query.
Anchor to Migrate page selection and bulk actionsMigrate page selection and bulk actions
The following index-table migration selects the current page. The same selected-ID set drives the header checkbox, row checkboxes, selected count, archive payload, and cleanup after success.
Migrating select-all actions
Polaris web components
import {useEffect, useMemo, useState} from 'react';
import {useRevalidator, useSearchParams} from 'react-router';
function ProductsIndex({products, pageInfo, archiveProducts}) {
const [searchParams, setSearchParams] = useSearchParams();
const revalidator = useRevalidator();
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [bulkPending, setBulkPending] = useState(false);
const query = searchParams.get('query') ?? '';
const status = searchParams.get('status') ?? 'all';
const sort = searchParams.get('sort') ?? 'title-asc';
const pageIds = useMemo(
() => products.map((product) => product.id),
[products],
);
const allOnPageSelected =
pageIds.length > 0 && pageIds.every((id) => selectedIds.has(id));
const someOnPageSelected =
!allOnPageSelected && pageIds.some((id) => selectedIds.has(id));
const selectionScope = ['query', 'status', 'sort', 'cursor', 'direction']
.map((name) => `${name}=${searchParams.get(name) ?? ''}`)
.join('&');
useEffect(() => {
setSelectedIds(new Set());
}, [selectionScope]);
useEffect(() => {
setSelectedIds(
(current) =>
new Set([...current].filter((id) => pageIds.includes(id))),
);
}, [pageIds]);
function updateView(
patch: Record<string, string | null>,
{replace = true}: {replace?: boolean} = {},
) {
const next = new URLSearchParams(searchParams);
next.delete('cursor');
next.delete('direction');
for (const [name, value] of Object.entries(patch)) {
if (value) {
next.set(name, value);
} else {
next.delete(name);
}
}
setSearchParams(next, {replace});
setSelectedIds(new Set());
}
function togglePage(checked) {
setSelectedIds((current) => {
const next = new Set(current);
for (const id of pageIds) {
checked ? next.add(id) : next.delete(id);
}
return next;
});
}
function toggleRow(id, checked) {
setSelectedIds((current) => {
const next = new Set(current);
checked ? next.add(id) : next.delete(id);
return next;
});
}
async function archiveSelection() {
const activeSelectedIds = pageIds.filter((id) => selectedIds.has(id));
if (activeSelectedIds.length === 0) return;
setBulkPending(true);
try {
await archiveProducts(activeSelectedIds);
shopify.toast.show(`${activeSelectedIds.length} products archived`);
setSelectedIds(new Set());
revalidator.revalidate();
} catch {
shopify.toast.show('Products couldn’t be archived', {isError: true});
} finally {
setBulkPending(false);
}
}
const showBulkActions = selectedIds.size > 0;
return (
<s-section padding="none" accessibilityLabel="Products">
<s-table
paginate
loading={pageInfo.loading}
hasPreviousPage={pageInfo.hasPreviousPage}
hasNextPage={pageInfo.hasNextPage}
onPreviousPage={() =>
updateView(
{cursor: pageInfo.startCursor, direction: 'previous'},
{replace: false},
)
}
onNextPage={() =>
updateView(
{cursor: pageInfo.endCursor, direction: 'next'},
{replace: false},
)
}
>
{showBulkActions ? (
<s-query-container slot="filters">
<s-box padding="small" background="strong">
<s-stack
direction="inline"
gap="small"
justifyContent="space-between"
>
<s-checkbox
checked={allOnPageSelected}
indeterminate={someOnPageSelected}
label={`${selectedIds.size} selected on this page`}
onChange={(event) =>
togglePage(event.currentTarget.checked)
}
/>
<s-button
variant="secondary"
loading={bulkPending}
onClick={archiveSelection}
>
Archive products
</s-button>
</s-stack>
</s-box>
</s-query-container>
) : (
<s-query-container slot="filters">
<s-stack gap="small">
<s-grid
gap="small"
alignItems="end"
gridTemplateColumns="repeat(auto-fit, minmax(12rem, 1fr))"
>
<s-search-field
label="Search"
value={query}
onInput={(event) =>
updateView({query: event.currentTarget.value})
}
/>
<s-select
label="Status"
value={status}
onChange={(event) =>
updateView({
status:
event.currentTarget.value === 'all'
? null
: event.currentTarget.value,
})
}
>
<s-option value="all">Any status</s-option>
<s-option value="active">Active</s-option>
<s-option value="draft">Draft</s-option>
</s-select>
<s-select
label="Sort by"
value={sort}
onChange={(event) =>
updateView({sort: event.currentTarget.value})
}
>
<s-option value="title-asc">Title, A–Z</s-option>
<s-option value="title-desc">Title, Z–A</s-option>
<s-option value="updated-desc">Recently updated</s-option>
</s-select>
</s-grid>
{(query || status !== 'all') && (
<s-stack direction="inline" gap="small">
{query && (
<s-clickable-chip
removable
accessibilityLabel="Remove search filter"
onRemove={() => updateView({query: null})}
>
Search: {query}
</s-clickable-chip>
)}
{status !== 'all' && (
<s-clickable-chip
removable
accessibilityLabel="Remove status filter"
onRemove={() => updateView({status: null})}
>
Status: {status}
</s-clickable-chip>
)}
</s-stack>
)}
</s-stack>
</s-query-container>
)}
<s-table-header-row>
<s-table-header listSlot="primary">
<s-stack direction="inline" gap="small" alignItems="center">
<s-checkbox
checked={allOnPageSelected}
indeterminate={someOnPageSelected}
accessibilityLabel="Select all products on this page"
onChange={(event) => togglePage(event.currentTarget.checked)}
/>
<s-text>Product</s-text>
</s-stack>
</s-table-header>
<s-table-header listSlot="secondary">Status</s-table-header>
</s-table-header-row>
<s-table-body>
{products.map((product) => {
const checkboxId = `select-${product.id}`;
return (
<s-table-row key={product.id} clickDelegate={checkboxId}>
<s-table-cell>
<s-stack direction="inline" gap="small" alignItems="center">
<s-checkbox
id={checkboxId}
checked={selectedIds.has(product.id)}
accessibilityLabel={`Select ${product.title}`}
onChange={(event) =>
toggleRow(product.id, event.currentTarget.checked)
}
/>
<s-link href={`/app/products/${product.id}`}>
{product.title}
</s-link>
</s-stack>
</s-table-cell>
<s-table-cell>
<s-badge
tone={product.status === 'active' ? 'success' : 'neutral'}
>
{product.status}
</s-badge>
</s-table-cell>
</s-table-row>
);
})}
</s-table-body>
</s-table>
</s-section>
);
}Polaris React
import {SelectAllActions} from '@shopify/polaris';
export function ProductSelection({selectedCount, itemCount, onToggleAll, archive}) {
const selected = selectedCount === itemCount;
return (
<SelectAllActions
selected={selected}
selectMode={selectedCount > 0}
paginatedSelectAllAction={{
content: `Select all ${itemCount} products`,
onAction: onToggleAll,
}}
paginatedSelectAllText={`${selectedCount} products selected`}
accessibilityLabel="Select all products"
onToggleAll={onToggleAll}
>
<button onClick={archive}>Archive products</button>
</SelectAllActions>
);
}Preview
| Polaris React | Polaris web components | Migration notes |
|---|---|---|
selected | checked on the select-all s-checkbox | Derive it by checking whether every selectable page ID is selected. |
| Partial selection | indeterminate on the checkbox | Set it when some, but not all, selectable page IDs are selected. |
onToggleAll | Checkbox onChange handler | Add or remove the relevant IDs from the same selected-ID set used by rows. |
selectMode | Conditionally render a bulk-action area | Show it when at least one resource is selected. |
paginatedSelectAllText | Explicit s-text with the selected count and scope | State whether selection covers this page or every matching result. |
paginatedSelectAllAction | A button that enters or exits query-selection mode | Store query selection separately from a finite set of loaded IDs. |
accessibilityLabel | accessibilityLabel or label on s-checkbox | Include the resource name and selection scope. |
disabled | disabled on selection and bulk-action controls | Exclude unavailable resource IDs from select-all calculations. |
children | Explicit bulk-action s-button controls or a menu | Pass the selected IDs or active query to each operation. |
Anchor to Implement current-page selectionImplement current-page selection
Derive pageIds from selectable records on the displayed page. The select-all checkbox is checked when pageIds isn't empty and every ID is in selectedIds. It's indeterminate when at least one, but not every, page ID is selected.
When the checkbox changes, add or remove every pageId in one state update. Don't maintain a separate allSelected boolean; it can disagree with row selection after one row changes.
If selection can persist across pages, keep already selected IDs when pagination changes and make the displayed count global. If it can't, clear selection when the cursor changes and describe the checkbox as selecting the current page.
Anchor to Implement all-results selectionImplement all-results selection
Don't represent every result as IDs loaded into the browser. Store a distinct query-selection model, for example:
In query mode, the backend operation receives the normalized search, filter, and sort scope plus explicit exclusions. Display the total matching count returned by the backend. When the query changes, exit query mode or require the merchant to confirm the new scope.
For destructive actions, show the actual scope in confirmation text, such as Archive 2,418 active products, not only Archive selected products.
Anchor to Handle bulk-action resultsHandle bulk-action results
Set the initiating button to pending and prevent repeat submissions. On success, clear selection, refresh results, and show confirmation. On failure, preserve selection and show retryable feedback.
Don't read selected IDs from checkbox elements when an action runs. The app-owned selection model must be the source for the count, control state, backend payload, and cleanup.
Anchor to Test the migrationTest the migration
- Select none, one, a partial page, the full page, multiple pages, and every query result where supported.
- Deselect one resource from full-page and query selection.
- Verify disabled resources aren't selected or included in counts.
- Change search, filters, sort, and pagination with active selection.
- Run each bulk action through pending, success, failure, and confirmation states.
- Verify checkbox names, indeterminate announcements, selected counts, and keyboard focus order.
Anchor to Remove Polaris ReactRemove Polaris React
After selection and all bulk actions are migrated together, remove SelectAllActions, duplicate allSelected flags, and Polaris-only action descriptors. Remove @shopify/polaris only after no other route in scope imports it.
- Migrate IndexTable from Polaris React
- Migrate ResourceList from Polaris React
- Migrate UnstableBulkActions from Polaris React