Skip to main content

Migrate Filters from Polaris React

Polaris web components don't have a descriptor-based Filters component. Compose s-search-field, form controls such as s-select, and removable s-clickable-chip elements around the same state that loads the resource view.

Use the resource index template when filters belong to a complete index page.


Anchor to Migrate search and filtersMigrate search and filters

The following example stores applied values in URL search parameters. Changing or removing a filter clears the existing pagination cursor before the app reloads results.

Migrating product filters

import {useEffect} from 'react';
import {useSearchParams} from 'react-router';

function ProductFilters({onResults, onRequestError}) {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('query') ?? '';
const status = searchParams.get('status') ?? 'all';
const requestKey = searchParams.toString();

useEffect(() => {
const controller = new AbortController();
const timeout = window.setTimeout(async () => {
try {
const response = await fetch(`/api/products?${requestKey}`, {
signal: controller.signal,
});
if (!response.ok) throw new Error('Product search failed');
onResults(await response.json());
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') return;
onRequestError(error);
}
}, 250);

return () => {
window.clearTimeout(timeout);
controller.abort();
};
}, [requestKey, onResults]);

function updateFilter(name: string, value: string | null) {
const next = new URLSearchParams(searchParams);
next.delete('cursor');
next.delete(name);

if (value) next.set(name, value);

setSearchParams(next, {replace: true});
}

function clearAll() {
const next = new URLSearchParams(searchParams);
next.delete('query');
next.delete('status');
next.delete('cursor');
setSearchParams(next, {replace: true});
}

return (
<s-stack gap="small-100">
<s-grid
gap="small"
alignItems="end"
gridTemplateColumns="repeat(auto-fit, minmax(12rem, 1fr))"
>
<s-search-field
label="Search"
value={query}
onInput={(event) =>
updateFilter('query', event.currentTarget.value || null)
}
/>
<s-select
label="Status"
value={status}
onChange={(event) =>
updateFilter(
'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-grid>
{(query || status !== 'all') && (
<s-stack direction="inline" gap="small">
{query && (
<s-clickable-chip
removable
accessibilityLabel="Remove search filter"
onRemove={() => updateFilter('query', null)}
>
Search: {query}
</s-clickable-chip>
)}
{status !== 'all' && (
<s-clickable-chip
removable
accessibilityLabel={`Remove ${status} status filter`}
onRemove={() => updateFilter('status', null)}
>
Status: {status}
</s-clickable-chip>
)}
<s-button variant="tertiary" onClick={clearAll}>
Clear all
</s-button>
</s-stack>
)}
</s-stack>
);
}
import {ChoiceList, Filters} from '@shopify/polaris';

export function ProductFilters({
query,
status,
onQueryChange,
onStatusChange,
onClearAll,
}) {
const filters = [
{
key: 'status',
label: 'Status',
filter: (
<ChoiceList
title="Status"
titleHidden
choices={[
{label: 'Active', value: 'active'},
{label: 'Draft', value: 'draft'},
]}
selected={status}
onChange={onStatusChange}
/>
),
},
];

const appliedFilters = status.length
? [{key: 'status', label: `Status: ${status[0]}`, onRemove: () => onStatusChange([])}]
: [];

return (
<Filters
queryValue={query}
queryPlaceholder="Search products"
onQueryChange={onQueryChange}
onQueryClear={() => onQueryChange('')}
filters={filters}
appliedFilters={appliedFilters}
onClearAll={onClearAll}
/>
);
}

Preview


Anchor to Replace filter descriptorsReplace filter descriptors

Polaris ReactPolaris web componentsMigration notes
queryValuevalue on s-search-fieldRead from the same source of truth used by the backend request.
onQueryChangeonInputUpdate the query and reset pagination.
onQueryClearSet the query value to null or an empty stringRemove the URL parameter and reload results.
queryPlaceholderplaceholderKeep an accessible label, even when it is visually exclusive.
filtersExplicit s-select, s-choice-list, date, or other field componentsChoose a control that matches each value rather than rendering descriptor objects.
appliedFiltersRemovable s-clickable-chip elementsRender the applied value and remove it from the same query state.
onClearAllOne handler that clears all filter parametersAlso reset pagination and selection.
disableddisabled on each controlPreserve the condition on the controls that it affects.

Don't keep a parallel appliedFilters array that can drift from backend parameters. Derive chips from the current query values.


Anchor to Keep one source of truthKeep one source of truth

When a filter changes:

  1. Update its URL or route-state value.
  2. Remove pagination cursors that belong to the previous query.
  3. Clear selected resource IDs that no longer have a defined relationship to the new results.
  4. Load results with the updated search, filter, and sort parameters.
  5. Render loading, empty, or error feedback for that request.

Anchor to Debounce and cancel remote requestsDebounce and cancel remote requests

The example updates the visible field and URL immediately, then waits 250 milliseconds before requesting results. Its effect creates an AbortController; cleanup clears the pending timer and aborts the previous fetch whenever the query changes. An aborted response is ignored, so it can't replace results for a newer query.

Keep loading and error state beside the results that the request owns. If your data framework already cancels route loaders, use that mechanism instead of adding a second fetch, but verify the same rapid-input behavior.


Anchor to Choose visible controls or a popoverChoose visible controls or a popover

Keep common filters visible beside search. Use a responsive grid with visible labels and bottom-aligned fields, then place applied chips directly below it. Use s-popover for secondary filter controls when space is constrained, but keep applied values visible as removable chips. Don't hide every filter in an overflow solely to match the old component's layout.

Use a self-contained label for every control. Preserve validation, date boundaries, option values, and serialization rules from the previous filter implementation.

For multi-value filters, use a stable repeated-key convention such as status=active&status=draft. Read it with searchParams.getAll('status'), replace all values together when the choice list changes, and derive one removable chip per value. If the backend expects another format, such as a comma-separated value, keep that format consistent across URL restoration, saved views, loader input, and chip removal.


  • Apply, remove, and clear every filter independently and in combination.
  • Reload and navigate through browser history, and verify that URL state restores the same results.
  • Change filters from a later page, and confirm that pagination resets.
  • Verify selection clears or reconciles according to the data-view rules.
  • Test slow, failed, empty, and out-of-order backend responses.
  • Verify labels, focus order, and chip removal with a keyboard and screen reader.

Anchor to Remove Polaris ReactRemove Polaris React

After every Filters call site is migrated, remove descriptor builders, duplicate applied-filter state, and the Filters import. Remove @shopify/polaris only after no other route in scope imports it.



Was this page helpful?