Skip to main content

Migrate SelectAllActions 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

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


Anchor to Replace SelectAllActions propertiesReplace SelectAllActions properties

Polaris ReactPolaris web componentsMigration notes
selectedchecked on the select-all s-checkboxDerive it by checking whether every selectable page ID is selected.
Partial selectionindeterminate on the checkboxSet it when some, but not all, selectable page IDs are selected.
onToggleAllCheckbox onChange handlerAdd or remove the relevant IDs from the same selected-ID set used by rows.
selectModeConditionally render a bulk-action areaShow it when at least one resource is selected.
paginatedSelectAllTextExplicit s-text with the selected count and scopeState whether selection covers this page or every matching result.
paginatedSelectAllActionA button that enters or exits query-selection modeStore query selection separately from a finite set of loaded IDs.
accessibilityLabelaccessibilityLabel or label on s-checkboxInclude the resource name and selection scope.
disableddisabled on selection and bulk-action controlsExclude unavailable resource IDs from select-all calculations.
childrenExplicit bulk-action s-button controls or a menuPass 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:

type Selection =
| {mode: 'ids'; ids: Set<string>}
| {mode: 'query'; query: ProductQuery; excludedIds: Set<string>};

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.


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



Was this page helpful?