Route loaders & SSR prefetch
Compute the same params in a loader so a prefetch hits the same cache key as the hook.
If you prefetch data in a route loader (TanStack Router, React Router) or on the
server, the loader has to compute the exact same params object the hook will
compute on mount. Otherwise the prefetch lands under a different cache key and the
page refetches anyway. resolveFilterParams is the framework-agnostic twin of the
hook's params derivation.
Use it in a loader
Give it the same config map and the request's search params:
// TanStack Router — search is already a parsed object
loader: ({ context: { queryClient }, location: { search } }) =>
queryClient.ensureQueryData(productQueryOptions(resolveFilterParams(productConfigs, search)));// React Router — pass the URLSearchParams straight in
export async function loader({ request }: LoaderFunctionArgs) {
const params = resolveFilterParams(productConfigs, new URL(request.url).searchParams);
return queryClient.ensureQueryData(productQueryOptions(params));
}It accepts the raw search in whatever shape your router gives you — a parsed object,
a URLSearchParams, or the raw location.search string — and applies the same
defaults, pagination mapping, and coercion as the hook (so ?status=5 becomes the
number 5, not '5'), then drops extras like async _label sidecars.
Keep the two in sync
For the query keys to match, the loader and the hook must use the same config
and the same options (arraySeparator, pagination). Sharing the config object
is easy; the options are easy to forget on one side:
// ⚠️ drift risk — the separator is set on one call but not the other
useFilters(productConfigs, { arraySeparator: '|' });
resolveFilterParams(productConfigs, search); // forgot arraySeparatordefineFilters binds the config and
those options once, so the hook and loader can't disagree. Reach for it whenever you
have a route loader.