useQueryParamsState
A custom React hook for easily managing and syncing values with the URL query string in Next.js (App Router).
Note: useQueryParamsState is a custom hook designed specifically for use in Next.js applications. It makes it easy to sync state with the URL query string, but is not part of the official Next.js documentation or API.
Quick overview
useQueryParamsState lets you read, set, and keep UI in sync with values from the browser's query parameters. It leverages Next.js's App Router navigation, is fully typesafe and handles optimistic updates. Useful for things like table filters, search terms, pagination, tabs, and any scenario where you want state in the URL.
Usage
Here's the entire hook, ready to copy–paste:
useQueryParamsState
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
import { useEffect, useCallback, useState, useTransition } from 'react';
const parseValue = <T,>(value: string | null): T | null => {
if (value === null) {
return null;
}
try {
return JSON.parse(value) as T;
} catch {
return value as T;
}
};
export function useQueryParamsState<T extends number | string = string>(
key: string,
defaultValue?: T,
) {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const [isPending, startTransition] = useTransition();
const urlValue = searchParams.get(key);
const parsedUrlValue = parseValue<T>(urlValue);
const [optimisticValue, setOptimisticValue] = useState<T | undefined>(() => {
return parsedUrlValue ?? defaultValue;
});
const createQueryString = useCallback(
(name: string, val: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set(name, val);
return params.toString();
},
[searchParams],
);
const deleteQueryParam = useCallback(
(name: string) => {
const params = new URLSearchParams(searchParams.toString());
params.delete(name);
return params.toString();
},
[searchParams],
);
useEffect(() => {
setOptimisticValue(parsedUrlValue ?? defaultValue);
}, [parsedUrlValue, defaultValue]);
useEffect(() => {
if (urlValue === null && defaultValue !== undefined) {
const queryString = createQueryString(key, String(defaultValue));
router.replace(`${pathname}?${queryString}`, { scroll: false });
}
}, []);
const setValue = useCallback(
(newValue: T | undefined) => {
setOptimisticValue(newValue);
startTransition(() => {
if (newValue === undefined) {
const queryString = deleteQueryParam(key);
router.push(
queryString ? `${pathname}?${queryString}` : pathname,
{ scroll: false }
);
} else {
const queryString = createQueryString(key, String(newValue));
router.push(`${pathname}?${queryString}`, { scroll: false });
}
});
},
[key, pathname, router, createQueryString, deleteQueryParam],
);
return [optimisticValue, setValue, isPending] as const;
}
Example
Example
const [tab, setTab] = useQueryParamsState<'details' | 'settings'>('tab', 'details');
// Reads "?tab=settings" from the URL by default (or "details" if not present)
// Updates the URL query string when calling setTab('settings')
Arguments
| Argument | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The URL query parameter name to store/read the state from |
defaultValue | T | No | Optional. The value to use if the URL parameter is missing. Will also initialize the URL when not set. |
- Name
key- Description
Type:
string
Required: Yes
The URL query parameter name to store/read the state from.
- Name
defaultValue- Description
Type:
T
Required: No
The value to use if the URL parameter is missing. Will also initialize the URL when not set.
Returns
This hook returns a tuple:
const [value, setValue, isPending] = useQueryParamsState(...)
value— The current value (from the URL or default).setValue(newValue)— Updates the value (and pushes to the URL).isPending—trueif a navigation from a state update is ongoing (for instant UI feedback).
Features
- Typesafe: accepts any
stringornumber(including union types, e.g.'foo' | 'bar').
JS: works with string & number out of the box. - Handles optimistic state updates for smooth UIs.
- Keeps state in sync with URL, even on navigations/popstate.
- Automatic initialization of the query if missing (if you provide a
defaultValue). - Uses Next.js App Router:
useSearchParams,useRouter, andusePathname.
Use cases
- Tab state (
?tab=details) - Table filters (
?sort=name) - Search boxes (
?q=hello) - Pagination (
?page=3)
What's next?
Great! You're ready to leverage useQueryParamsState in your UI for a more shareable and bookmark-friendly experience. Here are related resources:
- 3D Button Component — Interactive button with sound effects
- Playing Card Component — Animated card with flip transitions
- Wheel Component — Interactive spinning wheel