---
title: Migrate SelectAllActions from Polaris React
description: >-
  Replace Polaris React SelectAllActions with app-owned page and query selection
  plus explicit bulk-action controls.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/select-all-actions
  md: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/select-all-actions.md
api_name: app-home
---

# 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](https://shopify.dev/docs/api/app-home/patterns/compositions/index-table) or [resource list](https://shopify.dev/docs/api/app-home/patterns/compositions/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.

***

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

```tsx
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

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

***

## Replace Select​All​Actions properties

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

***

## Implement 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.

***

## Implement all-results selection

Don't represent every result as IDs loaded into the browser. Store a distinct query-selection model, for example:

```ts
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**.

***

## Handle 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.

***

## Test 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.

***

## Remove 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.

***

## Related guidance

* [Migrate IndexTable from Polaris React](https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/index-table)
* [Migrate ResourceList from Polaris React](https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/resource-list)
* [Migrate UnstableBulkActions from Polaris React](https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/unstable-bulk-actions)

***
