Recipes

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.

© Vojtěch Václav Porteš 2026 - All library contents are available under the MIT license.
Loading privacy preferences...