---
title: Migrate ResourceItem from Polaris React
description: >-
  Replace Polaris React ResourceItem with an explicit row composition that
  preserves resource navigation, selection, media, secondary content, disabled
  state, and item actions.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/resource-item
  md: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/resource-item.md
api_name: app-home
---

# Migrate Resource​Item from Polaris React

Polaris web components don't have a like-for-like `ResourceItem`. Compose each item inside the [resource list pattern](https://shopify.dev/docs/api/app-home/patterns/compositions/resource-list), and keep its navigation, selection, content, and actions as separate controls.

Migrate `ResourceItem` with its containing `ResourceList`. Selection and bulk actions depend on the complete set of visible resource IDs, not an isolated row.

***

## Migrate a resource item

The Polaris web components example includes the complete list so that the row's checkbox, select-all state, item actions, filters, and pagination use one state model.

## Migrating customer resource items

##### Polaris web components

```tsx
import {useEffect, useMemo, useState} from 'react';
import {useRevalidator, useSearchParams} from 'react-router';

function CustomersResourceList({customers, pageInfo, archiveCustomers}) {
  const [searchParams, setSearchParams] = useSearchParams();
  const revalidator = useRevalidator();
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
  const [archivedIds, setArchivedIds] = useState<Set<string>>(new Set());
  const [archivePending, setArchivePending] = useState(false);
  const [archivePendingId, setArchivePendingId] = useState<string | null>(null);

  const query = searchParams.get('query') ?? '';
  const status = searchParams.get('status') ?? 'all';
  const visibleCustomers = useMemo(
    () => customers.filter((customer) => !archivedIds.has(customer.id)),
    [archivedIds, customers],
  );
  const pageIds = useMemo(
    () =>
      visibleCustomers
        .filter((customer) => !customer.disabled)
        .map((customer) => customer.id),
    [visibleCustomers],
  );
  const allOnPageSelected =
    pageIds.length > 0 && pageIds.every((id) => selectedIds.has(id));
  const someOnPageSelected =
    !allOnPageSelected && pageIds.some((id) => selectedIds.has(id));
  const selectionScope = ['query', 'status', '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(name: string, value: string | null) {
    const next = new URLSearchParams(searchParams);
    next.delete('cursor');
    next.delete('direction');

    if (value && value !== 'all') {
      next.set(name, value);
    } else {
      next.delete(name);
    }

    setSearchParams(next, {replace: true});
    setSelectedIds(new Set());
  }

  function togglePage(checked: boolean) {
    setSelectedIds((current) => {
      const next = new Set(current);
      pageIds.forEach((id) => (checked ? next.add(id) : next.delete(id)));
      return next;
    });
  }

  function toggleCustomer(id: string, checked: boolean) {
    setSelectedIds((current) => {
      const next = new Set(current);
      checked ? next.add(id) : next.delete(id);
      return next;
    });
  }

  function changePage(cursor: string, direction: 'previous' | 'next') {
    const next = new URLSearchParams(searchParams);
    next.set('cursor', cursor);
    next.set('direction', direction);
    setSearchParams(next);
    setSelectedIds(new Set());
  }

  async function archiveSelection() {
    const ids = pageIds.filter((id) => selectedIds.has(id));
    if (ids.length === 0) return;

    setArchivePending(true);
    try {
      await archiveCustomers(ids);
      setArchivedIds((current) => new Set([...current, ...ids]));
      setSelectedIds(new Set());
      revalidator.revalidate();
      shopify.toast.show(`${ids.length} customers archived`);
    } catch {
      shopify.toast.show("Customers couldn't be archived", {isError: true});
    } finally {
      setArchivePending(false);
    }
  }

  async function archiveCustomer(id: string, name: string) {
    setArchivePendingId(id);
    try {
      await archiveCustomers([id]);
      setArchivedIds((current) => new Set(current).add(id));
      setSelectedIds((current) => {
        const next = new Set(current);
        next.delete(id);
        return next;
      });
      revalidator.revalidate();
      shopify.toast.show(`${name} archived`);
    } catch {
      shopify.toast.show(`${name} couldn't be archived`, {isError: true});
    } finally {
      setArchivePendingId(null);
    }
  }

  return (
    <s-section padding="none" accessibilityLabel="Customers">
      <s-stack gap="none">
        <s-table
          paginate
          loading={pageInfo.loading}
          hasPreviousPage={pageInfo.hasPreviousPage}
          hasNextPage={pageInfo.hasNextPage}
          onPreviousPage={() =>
            changePage(pageInfo.startCursor, 'previous')
          }
          onNextPage={() => changePage(pageInfo.endCursor, 'next')}
        >
          {selectedIds.size > 0 ? (
            <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
                    loading={archivePending}
                    onClick={archiveSelection}
                  >
                    Archive customers
                  </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 || null)
                    }
                  />
                  <s-select
                    label="Status"
                    value={status}
                    onChange={(event) =>
                      updateView('status', event.currentTarget.value)
                    }
                  >
                    <s-option value="all">Any status</s-option>
                    <s-option value="active">Active</s-option>
                    <s-option value="disabled">Disabled</s-option>
                  </s-select>
                </s-grid>
              </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}
                  disabled={pageIds.length === 0}
                  accessibilityLabel={`Select all ${pageIds.length} customers on this page`}
                  onChange={(event) =>
                    togglePage(event.currentTarget.checked)
                  }
                />
                <s-text>Customer</s-text>
              </s-stack>
            </s-table-header>
            <s-table-header listSlot="labeled">Location</s-table-header>
            <s-table-header listSlot="secondary">Actions</s-table-header>
          </s-table-header-row>

          <s-table-body>
            {visibleCustomers.map((customer) => {
              const menuId = `customer-actions-${customer.id}`;
              return (
                <s-table-row key={customer.id}>
                  <s-table-cell>
                    <s-stack direction="inline" gap="small" alignItems="center">
                      <s-checkbox
                        checked={selectedIds.has(customer.id)}
                        disabled={customer.disabled}
                        accessibilityLabel={`Select ${customer.name}`}
                        onChange={(event) =>
                          toggleCustomer(
                            customer.id,
                            event.currentTarget.checked,
                          )
                        }
                      />
                      <s-avatar
                        alt={customer.name}
                        initials={customer.initials}
                      />
                      {customer.disabled ? (
                        <s-text type="strong">{customer.name}</s-text>
                      ) : (
                        <s-link href={`/app/customers/${customer.id}`}>
                          {customer.name}
                        </s-link>
                      )}
                    </s-stack>
                  </s-table-cell>
                  <s-table-cell>{customer.location}</s-table-cell>
                  <s-table-cell>
                    <s-button
                      icon="menu-horizontal"
                      variant="tertiary"
                      accessibilityLabel={`Actions for ${customer.name}`}
                      commandFor={menuId}
                    />
                    <s-menu
                      id={menuId}
                      accessibilityLabel={`Actions for ${customer.name}`}
                    >
                      {!customer.disabled && (
                        <s-button href={`/app/customers/${customer.id}/edit`}>
                          Edit customer
                        </s-button>
                      )}
                      <s-button
                        tone="critical"
                        disabled={customer.disabled}
                        loading={archivePendingId === customer.id}
                        onClick={() =>
                          archiveCustomer(customer.id, customer.name)
                        }
                      >
                        Archive customer
                      </s-button>
                    </s-menu>
                  </s-table-cell>
                </s-table-row>
              );
            })}
          </s-table-body>
        </s-table>

        {!pageInfo.loading && visibleCustomers.length === 0 && (
          <s-box padding="large">
            <s-stack gap="small" alignItems="center">
              <s-heading>No customers found</s-heading>
              <s-paragraph>Clear the filters or try another search.</s-paragraph>
              <s-button
                onClick={() => {
                  setSearchParams(new URLSearchParams(), {replace: true});
                  setSelectedIds(new Set());
                }}
              >
                Clear filters
              </s-button>
            </s-stack>
          </s-box>
        )}
      </s-stack>
    </s-section>
  );
}
```

##### Polaris React

```tsx
import {Avatar, ResourceItem, Text} from '@shopify/polaris';

export function CustomerItem({customer, archiveCustomer}) {
  return (
    <ResourceItem
      id={customer.id}
      url={`/app/customers/${customer.id}`}
      external={false}
      media={<Avatar name={customer.name} initials={customer.initials} />}
      accessibilityLabel={`View ${customer.name}`}
      disabled={customer.disabled}
      shortcutActions={[
        {content: 'Edit customer', url: `/app/customers/${customer.id}/edit`},
        {
          content: 'Archive customer',
          destructive: true,
          onAction: () => archiveCustomer(customer.id),
        },
      ]}
    >
      <Text as="h3" fontWeight="semibold">{customer.name}</Text>
      <div>{customer.location}</div>
    </ResourceItem>
  );
}
```

***

## Replace Resource​Item properties

| Polaris React | Polaris web components | Migration notes |
| - | - | - |
| `id` | Stable app-owned ID and React `key` | Use the same ID for selection and backend action payloads. |
| `url` | `href` on the primary `s-link` | Keep resource navigation as a real link. |
| `external` | `target="_blank"` on the link when navigation truly leaves the embedded app | Don't open internal app routes in a new tab. |
| `onClick` | `onClick` on an explicit `s-button` | Use a button only when the row performs an action instead of navigation. |
| `media` | `s-avatar`, `s-thumbnail`, or `s-image` in its own grid cell | Preserve fallback text and alternative text. |
| `children` | Explicit `s-heading`, `s-text`, `s-paragraph`, badge, and layout children | Keep the name and essential state scannable. |
| `accessibilityLabel` and `name` | Visible link text and control-specific `accessibilityLabel` values | Name selection and action controls separately. |
| `shortcutActions` | An action trigger and sibling `s-menu`, or visible buttons | Use visible actions when they are frequent or primary. |
| `persistActions` | Keep essential actions visible | Don't force persistent actions into an overflow menu. |
| `disabled` | Disable selection and actions, and remove navigation when the item can't be opened | Preserve the reason in visible supporting content when merchants need it. |
| Selection inherited from `ResourceList` | A row `s-checkbox` backed by the list's selected-ID set | Don't give the row a second selection state. |

If the old item uses `onClick` and `url` together, decide which behavior is primary. Don't make a link also run an unrelated mutation. Put the mutation on a separate button or menu item.

***

## Preserve item actions

Convert `shortcutActions` descriptors to explicit controls. Map `content` to button text, `url` to `href`, `onAction` to `onClick`, `disabled` to `disabled`, and `destructive` to `tone="critical"`.

When the action is asynchronous, set pending state on the initiating control, prevent duplicate submissions, show success or failure feedback, and update the resource or list data. Confirm destructive actions that are difficult to reverse.

***

## Test the migration

* Open internal and external resource links with pointer, keyboard, and modified clicks.
* Select and deselect enabled items and verify disabled items can't enter the selected-ID set.
* Exercise every item action through success, failure, and pending states.
* Test missing images, long names, long secondary content, and narrow containers.
* Verify that link, checkbox, and menu controls have distinct names and focus stops.

***

## Remove Polaris React

After all items and their containing list are migrated, remove `ResourceItem`, related Polaris action descriptors, and wrappers used only for row layout. Remove `@shopify/polaris` only after no other route in scope imports it.

***

## Related guidance

* [Migrate ResourceList from Polaris React](https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/resource-list)
* [Resource list pattern](https://shopify.dev/docs/api/app-home/patterns/compositions/resource-list)
* [Menu component](https://shopify.dev/docs/api/app-home/web-components/actions/menu)
* [Modal API](https://shopify.dev/docs/api/app-home/apis/user-interface-and-interactions/modal-api)

***
