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
Polaris web components
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>
);
}Polaris React
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 React | Polaris web components | Migration notes |
|---|---|---|
queryValue | value on s-search-field | Read from the same source of truth used by the backend request. |
onQueryChange | onInput | Update the query and reset pagination. |
onQueryClear | Set the query value to null or an empty string | Remove the URL parameter and reload results. |
queryPlaceholder | placeholder | Keep an accessible label, even when it is visually exclusive. |
filters | Explicit s-select, s-choice-list, date, or other field components | Choose a control that matches each value rather than rendering descriptor objects. |
appliedFilters | Removable s-clickable-chip elements | Render the applied value and remove it from the same query state. |
onClearAll | One handler that clears all filter parameters | Also reset pagination and selection. |
disabled | disabled on each control | Preserve 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:
- Update its URL or route-state value.
- Remove pagination cursors that belong to the previous query.
- Clear selected resource IDs that no longer have a defined relationship to the new results.
- Load results with the updated search, filter, and sort parameters.
- 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.
Anchor to Test the migrationTest the migration
- 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.
- Resource index template
- Search field component
- Migrate IndexTable from Polaris React
- Migrate Popover from Polaris React