Skip to main content

useQueryString

The useQueryString hook provides a convenient way to create and manipulate URL query strings. It takes the current URLSearchParams and returns a function to generate new query strings with updated parameters.

Installation

npm install @craft-ui/hooks

Import

import { useQueryString } from "@craft-ui/hooks";

Usage

import { useQueryString } from "@craft-ui/hooks";
import { useSearchParams, useRouter } from "next/navigation";

function SearchFilters() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const { createQueryString } = useQueryString(searchParams);

  const updateFilter = (key: string, value: string) => {
    const newQueryString = createQueryString({ [key]: value });
    router.push(`?${newQueryString}`);
  };

  return (
    <div>
      <button onClick={() => updateFilter("category", "electronics")}>
        Electronics
      </button>
      <button onClick={() => updateFilter("sort", "price")}>
        Sort by Price
      </button>
    </div>
  );
}

Parameters

searchParams
URLSearchParams
required
The current URL search parameters, typically obtained from Next.js useSearchParams() or browser’s window.location.search.

Returns

createQueryString
(params: QueryParams) => string
A memoized function that accepts an object of query parameters and returns a new query string. Parameters with null values are removed from the query string.

QueryParams Type

type QueryParams = Record<string, string | number | null>;
  • string or number values are added/updated in the query string
  • null values remove the parameter from the query string

Type Definition

type QueryParams = Record<string, string | number | null>;

function useQueryString(searchParams: URLSearchParams): {
  createQueryString: (params: QueryParams) => string;
};

Examples

Product Filtering

import { useQueryString } from "@craft-ui/hooks";
import { useSearchParams, useRouter } from "next/navigation";

function ProductFilters() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const { createQueryString } = useQueryString(searchParams);

  const setFilter = (filters: Record<string, string | null>) => {
    router.push(`/products?${createQueryString(filters)}`);
  };

  return (
    <div>
      <select
        onChange={(e) => setFilter({ category: e.target.value || null })}
        value={searchParams.get("category") || ""}
      >
        <option value="">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
        <option value="books">Books</option>
      </select>

      <select
        onChange={(e) => setFilter({ sort: e.target.value || null })}
        value={searchParams.get("sort") || ""}
      >
        <option value="">Default Sort</option>
        <option value="price-asc">Price: Low to High</option>
        <option value="price-desc">Price: High to Low</option>
        <option value="rating">Highest Rated</option>
      </select>

      <button onClick={() => setFilter({ category: null, sort: null })}>
        Clear Filters
      </button>
    </div>
  );
}

Pagination

import { useQueryString } from "@craft-ui/hooks";
import { useSearchParams, useRouter } from "next/navigation";

function Pagination({ totalPages }: { totalPages: number }) {
  const router = useRouter();
  const searchParams = useSearchParams();
  const { createQueryString } = useQueryString(searchParams);
  const currentPage = Number(searchParams.get("page")) || 1;

  const goToPage = (page: number) => {
    router.push(`?${createQueryString({ page })}`);
  };

  return (
    <div>
      <button
        onClick={() => goToPage(currentPage - 1)}
        disabled={currentPage === 1}
      >
        Previous
      </button>
      <span>Page {currentPage} of {totalPages}</span>
      <button
        onClick={() => goToPage(currentPage + 1)}
        disabled={currentPage === totalPages}
      >
        Next
      </button>
    </div>
  );
}

Search with Filters

import { useQueryString } from "@craft-ui/hooks";
import { useSearchParams, useRouter } from "next/navigation";
import { useState } from "react";

function SearchBar() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const { createQueryString } = useQueryString(searchParams);
  const [query, setQuery] = useState(searchParams.get("q") || "");

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault();
    router.push(`/search?${createQueryString({ q: query, page: 1 })}`);
  };

  const clearSearch = () => {
    setQuery("");
    router.push(`/search?${createQueryString({ q: null, page: null })}`);
  };

  return (
    <form onSubmit={handleSearch}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <button type="submit">Search</button>
      {query && <button type="button" onClick={clearSearch}>Clear</button>}
    </form>
  );
}

Multi-Select Filters

import { useQueryString } from "@craft-ui/hooks";
import { useSearchParams, useRouter } from "next/navigation";

function TagFilters({ availableTags }: { availableTags: string[] }) {
  const router = useRouter();
  const searchParams = useSearchParams();
  const { createQueryString } = useQueryString(searchParams);
  
  const selectedTags = searchParams.get("tags")?.split(",").filter(Boolean) || [];

  const toggleTag = (tag: string) => {
    const newTags = selectedTags.includes(tag)
      ? selectedTags.filter((t) => t !== tag)
      : [...selectedTags, tag];

    router.push(
      `?${createQueryString({
        tags: newTags.length > 0 ? newTags.join(",") : null,
      })}`
    );
  };

  return (
    <div>
      {availableTags.map((tag) => (
        <button
          key={tag}
          onClick={() => toggleTag(tag)}
          style={{
            fontWeight: selectedTags.includes(tag) ? "bold" : "normal",
          }}
        >
          {tag}
        </button>
      ))}
    </div>
  );
}

Date Range Filter

import { useQueryString } from "@craft-ui/hooks";
import { useSearchParams, useRouter } from "next/navigation";

function DateRangeFilter() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const { createQueryString } = useQueryString(searchParams);

  const startDate = searchParams.get("startDate") || "";
  const endDate = searchParams.get("endDate") || "";

  const updateDateRange = (start: string, end: string) => {
    router.push(
      `?${createQueryString({
        startDate: start || null,
        endDate: end || null,
      })}`
    );
  };

  return (
    <div>
      <input
        type="date"
        value={startDate}
        onChange={(e) => updateDateRange(e.target.value, endDate)}
      />
      <input
        type="date"
        value={endDate}
        onChange={(e) => updateDateRange(startDate, e.target.value)}
      />
      <button onClick={() => updateDateRange("", "")}>
        Clear Dates
      </button>
    </div>
  );
}

Tab Navigation

import { useQueryString } from "@craft-ui/hooks";
import { useSearchParams, useRouter } from "next/navigation";

function TabNavigation() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const { createQueryString } = useQueryString(searchParams);
  const activeTab = searchParams.get("tab") || "overview";

  const tabs = ["overview", "details", "reviews", "specifications"];

  return (
    <div>
      {tabs.map((tab) => (
        <button
          key={tab}
          onClick={() => router.push(`?${createQueryString({ tab })}`)
          }
          style={{
            fontWeight: activeTab === tab ? "bold" : "normal",
          }}
        >
          {tab.charAt(0).toUpperCase() + tab.slice(1)}
        </button>
      ))}
    </div>
  );
}

Common Patterns

Preserving Existing Parameters

The hook automatically preserves existing parameters:
// Current URL: /products?category=electronics&page=2
const newQuery = createQueryString({ sort: "price" });
// Result: "category=electronics&page=2&sort=price"

Removing Parameters

Use null to remove parameters:
const newQuery = createQueryString({ filter: null });
// Removes the 'filter' parameter from the query string

Multiple Updates

Update multiple parameters at once:
const newQuery = createQueryString({
  category: "books",
  sort: "rating",
  page: 1,
});

Reset All Filters

const resetFilters = () => {
  const newQuery = createQueryString({
    category: null,
    sort: null,
    page: null,
    search: null,
  });
  router.push(`?${newQuery}`);
};

Use Cases

  • Building search and filter interfaces
  • Implementing pagination
  • Managing tab navigation with URL state
  • Creating shareable URLs with filter states
  • Handling sort options
  • Multi-select filters
  • Date range selectors
  • Any scenario requiring URL query parameter manipulation

Notes

  • The hook is designed to work with Next.js App Router’s useSearchParams
  • The createQueryString function is memoized and only recreates when searchParams changes
  • Numeric values are automatically converted to strings in the query string
  • Setting a parameter to null removes it from the query string
  • The hook preserves all existing parameters unless explicitly overridden or set to null
  • The returned query string does not include the leading ? character
  • This hook does not modify the URL directly; you need to use it with a router or navigation method

Build docs developers (and LLMs) love