Persist React Filters in URL Query Parameters
Encode validated filter state in a shareable URL and restore it on navigation.
What this recipe builds
- Back/forward-friendly filter state.
- Shareable links with a safe fallback.
- A single decoder that can migrate older URL formats later.
Loading the interactive recipe demo...
Install and imports
Install / imports
npm install @vojtechportes/react-query-builder
Fields and initial query
Typed field configuration
const fields: IBuilderFieldProps[] = [{ field: 'status', label: 'Status', type: 'LIST', value: statusOptions },{ field: 'owner', label: 'Owner', type: 'TEXT' },];const fallbackQuery: DenormalizedQuery = [{type: 'GROUP',value: 'AND',isNegated: false,children: [],},];
Builder implementation
React implementation
import '@vojtechportes/react-query-builder/styles.css';import React from 'react';import {Builder,type DenormalizedQuery,type IBuilderFieldProps,} from '@vojtechportes/react-query-builder';const fields: IBuilderFieldProps[] = [{field: 'status',label: 'Status',type: 'LIST',value: [{ label: 'Open', value: 'OPEN' },{ label: 'Closed', value: 'CLOSED' },],},{ field: 'owner', label: 'Owner', type: 'TEXT' },];const fallbackQuery: DenormalizedQuery = [{ type: 'GROUP', value: 'AND', isNegated: false, children: [] },];const isRecord = (value: unknown): value is Record<string, unknown> =>typeof value === 'object' && value !== null;const isQueryNode = (value: unknown): boolean => {if (!isRecord(value)) return false;if (value.type === 'GROUP') {return ((value.value === 'AND' || value.value === 'OR') &&Array.isArray(value.children) &&value.children.every(isQueryNode));}return typeof value.field === 'string' && typeof value.operator === 'string';};const validateFilter = (value: unknown): DenormalizedQuery | undefined =>Array.isArray(value) && value.every(isQueryNode)? (value as DenormalizedQuery): undefined;const readFilter = (search: string): DenormalizedQuery | undefined => {try {const value = new URLSearchParams(search).get('filter');return value ? validateFilter(JSON.parse(value)) : undefined;} catch {return undefined;}};export const UrlFilter = () => {const [query, setQuery] = React.useState(() => readFilter(location.search) ?? fallbackQuery);React.useEffect(() => {const params = new URLSearchParams(location.search);params.set('filter', JSON.stringify(query));history.replaceState(null, '', '?' + params.toString());}, [query]);return <Builder fields={fields} data={query} onChange={setQuery} />;};
Decode and validate on page load
Decode and validate on page load
const readFilter = (search: string): DenormalizedQuery | undefined => {try {const value = new URLSearchParams(search).get('filter');return value ? validateFilter(JSON.parse(value)) : undefined;} catch {return undefined;}};
Validation and safety
- Treat URL data as untrusted and reject unknown fields, operators, excessive depth, and oversized values.
- Never put secrets or access-control rules in the URL.
Production notes
- Use replaceState while editing so every change does not add a browser-history entry. Use pushState when the user explicitly saves a search.
- Move large filters to saved server-side presets and keep only the preset ID in the URL.
Related guides
Frequently asked questions
Can every filter be stored in the URL?
URL length limits vary by browser. A large React Query Builder filter can exceed that limit, so be careful with this approach. For complex filters, store the filter elsewhere and put only its preset ID in the URL.