Content Manager URL & Filter State#
Overview#
The Content Manager's ListView encodes all view state — filters, pagination, sort, locale, and other plugin parameters — in the URL query string. This makes individual views deep-linkable and bookmarkable. State flows from user interaction → URL update → API request in a unidirectional pattern mediated by useQueryParams and a qs-based serialization layer.
URL Shape#
The query string uses the qs library for serialization/deserialization, enabling deeply nested objects (e.g., filters[$and][0][status][$eq]=draft). The top-level keys recognised by the ListView are typed as ListViewQuery:
| Key | Purpose |
|---|---|
filters.$and | Array of filter objects, each { [field]: { [operator]: value } } |
plugins.* | Plugin-specific params (e.g., plugins.i18n.locale); stripped before API calls |
page | Current page number (string) |
pageSize | Entries per page (string) |
sort | Sort expression, e.g., "name:ASC" or "createdAt:DESC" |
Core Hook: useQueryParams#
useQueryParams (packages/core/admin/admin/src/hooks/useQueryParams.ts) is the shared foundation:
- Reads the current
location.search, stripping any leading?, and merges it with optionalinitialParamsviaqs.parse. - Writes via
setQuery(nextParams, method, replace):method='push'(default) mergesnextParamsinto the current query.method='remove'deletes the specified keys.- Calls
navigate({ search: stringify(nextQuery, { encode: false }) }, { replace }).
- Returns
[{ query, rawQuery }, setQuery].
The ListView calls it with defaults for page, pageSize, and sort .
Persistent State: usePersistentPartialQueryParams#
usePersistentPartialQueryParams (packages/core/content-manager/admin/src/hooks/usePersistentQueryParams.ts) layers localStorage persistence on top of useQueryParams:
- Accepts a
PersistentQueryConfig— a map of storage keys to{ paths, scoped? }. Keys scoped to a model are namespacedKEY:modelUID. - On mount, reads matching keys from
localStorage, deep-merges with URL params (URL wins), and navigates to the merged state. SetsisHydrated=trueafter this . - On query change, serializes only the configured paths back to
localStorage. - Returns
{ isHydrated }. The ListView delays the API fetch untilisHydratedis true to avoid a flash of un-restored state .
The ListView configures two persistent entries :
STRAPI_LIST_VIEW_SETTINGS:<model> → paths: ['sort', 'filters', 'pageSize'], scoped: true
STRAPI_LOCALE → paths: ['plugins.i18n.locale'], scoped: false
Filter State Flow#
Filter Serialization (Popover → URL)#
Filters.Popover.handleSubmit builds the filter entry:
- Values are
encodeURIComponent-encoded (filter-free operators like$nullget"true"instead). - Relations are nested:
{ [field]: { [mainField|id]: { [op]: value } } }. - The new entry is appended (or replaces when editing) in
query.filters.$and, then pushed to the URL viasetQuery({ filters: newFilterQuery, page: 1 }, 'push', true).
Filter Deserialization (URL → display)#
Filters.List reads query.filters.$and and renders an AttributeTag for each entry. Values are decoded with decodeURIComponent before display . Clicking a tag removes it from $and and resets page to 1.
Schema-Aware Filter Options#
The ListView wraps Filters with a custom listViewFilters.Root that:
- Derives a
displayedFiltersarray from the content type schema and layout. - Adds
id,documentId, and (for D&P content types) a special__statusfilter . - Runs the
INJECT_LIST_VIEW_FILTERShook waterfall so plugins can add filters . - Passes the resolved filter options into the base
Filters.Rootasoptions.
Status Sort Interaction#
When a __status filter is active, the status column sort is disabled. If sort=status:* is already in the URL when a status filter is applied, an effect in ListViewPage strips it from the query .
URL → API Request: buildValidParams#
Before calling the API, the raw query is passed through buildValidParams (packages/core/content-manager/admin/src/utils/api.ts):
- Removes the
pluginskey. - Promotes each plugin's values to the root level (e.g.,
plugins.i18n.locale=en→locale=en). - The resulting flat object is sent as API query params to
useGetAllDocumentsQuery.
Edge Cases & Known Fixes#
- Trailing
?in URLs: Whenqs.stringify({})returns"", theparamsSerializeringetFetchClient.tswas previously emitting?, which broke publish deduplication guards. Fixed by skipping the?when the serialized result is empty . - Hydration guard: API calls are skipped while
isHydrated=falseto prevent the page loading beforelocalStoragestate is restored . - Over-range pagination: If the API returns a
pagination.pagebeyondpagination.pageCount, the page is reset topageCountvia anavigate({ replace: true }). - Plugin params propagation: When navigating to create/edit entries, only
query.pluginsis forwarded (not sort/filters), preserving locale without carrying filter state into the edit view .
Key Files#
| File | Role |
|---|---|
ListViewPage.tsx | Top-level orchestration: query defaults, hydration guard, API call, status-sort interaction |
useQueryParams.ts | qs-based URL read/write hook used across the admin |
usePersistentQueryParams.ts | localStorage persistence layer for URL params |
Filters.tsx (admin) | Generic filter UI: Root, Trigger, Popover, List |
Filters.tsx (content-manager) | Schema-aware filter wrapper with listViewFilters export |
api.ts | buildValidParams — strips plugins.*, promotes plugin values to root |