useQueryParamsState

A custom React hook for easily managing and syncing values with the URL query string in Next.js (App Router).

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

ArgumentTypeRequiredDescription
keystringYesThe URL query parameter name to store/read the state from
defaultValueTNoOptional. 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).
  • isPendingtrue if a navigation from a state update is ongoing (for instant UI feedback).

Features

  • Typesafe: accepts any string or number (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, and usePathname.

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:

Was this page helpful?