npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

next-url-state

v1.0.7

Published

React hooks for managing state in URL search parameters with Next.js

Readme

next-url-state

Next.js URL state management that gets out of your way.

npm version npm downloads GitHub stars Bundle size License: MIT

Preview (Video)

Used in production by:

Table of Contents

Why you should use this library?

The Problem with Vanilla Next.js

Managing URL state in Next.js requires a lot of boilerplate and comes with several pain points

Problems:

  • Lots of boilerplate - Manual state synchronization, useEffect hooks, query object spreading
  • Race conditions - State and URL can get out of sync during navigation
  • Performance issues - Every keystroke triggers a router update, causing re-renders
  • Type safety - URL params are always string | string[] | undefined, requiring type guards
  • Complex updates - Managing multiple parameters requires careful query object manipulation
  • Router differences - Different APIs for Pages Router (next/router) vs App Router (next/navigation)

The Solution with next-url-state is slim and simple

Benefits:

  • Minimal code - One line hook replaces 20+ lines of boilerplate
  • Automatic sync - State and URL stay in sync automatically
  • Optimized performance - Built-in batching ( default: 250ms, configurable ) and optimistic updates
  • Type safety & Flexibility - Support for basic and custom data types, including custom serialization
  • Simple API - Works just like useState but with URL persistence
  • Router agnostic - Works with both Pages Router and App Router — hooks have identical API regardless of router
  • Zero Dependencies - Lightweight with only peer dependencies on React and Next.js

This library handles all the complexity of URL state management, letting you focus on building features instead of wrestling with router APIs.

How It Compares

| Feature | next-url-state | nuqs | use-query-params | |---|:---:|:---:|:---:| | Pages Router (/pages) | ✅ | ✅ | ✅ | | App Router (/app) | ✅ | ✅ | ⚠️ adapter required | | React Server Components | ✅ read-only | ✅ read-only | ❌ | | Works like useState | ✅ | ✅ | ✅ | | Optimistic UI updates | ✅ | ❌ | ❌ | | Automatic batching | ✅ | ✅ | ❌ | | Custom parse / serialize | ✅ | ✅ | ✅ | | TypeScript support | ✅ | ✅ | ✅ | | Zero dependencies | ✅ | ❌ | ❌ | | Bundle size (min+gzip) | | | |

⚠️ Competitor features reflect best available knowledge — check their docs for the latest.

Installation

npm install next-url-state
yarn add next-url-state
pnpm add next-url-state

Quick Start

30-Second Start

Two steps — pick the right provider for your router:

// Step 1 — wrap your app once
// pages/_app.tsx (Pages Router)
import { UrlParamsPagesRouterProvider } from 'next-url-state';

export default function App({ Component, pageProps }) {
  return (
    <UrlParamsPagesRouterProvider>
      <Component {...pageProps} />
    </UrlParamsPagesRouterProvider>
  );
}
// app/layout.tsx (App Router)
import { UrlParamsProvider } from 'next-url-state';

export default function RootLayout({ children }) {
  return (
    <html><body>
      <UrlParamsProvider>{children}</UrlParamsProvider>
    </body></html>
  );
}
// Step 2 — use it anywhere in both routers, just like useState
'use client'; // only needed for App Router
import { useUrlParam } from 'next-url-state';

function Search() {
  const [query, setQuery] = useUrlParam('q');
  return <input value={query ?? ''} onChange={e => setQuery(e.target.value)} />;
}

That's it. The URL updates automatically. For router-specific setup details see below.


Pages Router Setup

1. Wrap your app with UrlParamsPagesRouterProvider

// pages/_app.tsx
import { UrlParamsPagesRouterProvider } from 'next-url-state';

const MyApp = ({ Component, pageProps }) => {
  return (
    <UrlParamsPagesRouterProvider>
      <Component {...pageProps} />
    </UrlParamsPagesRouterProvider>
  );
}

export default MyApp;

Why UrlParamsPagesRouterProvider and not UrlParamsProvider?

UrlParamsPagesRouterProvider is a thin wrapper that reads router.asPath from next/router and passes it to the provider as the initial server-side path. This means URL parameters are available during SSR, so the server and client render identically — no hydration mismatch.

UrlParamsProvider is intended for App Router apps — use it there, not in Pages Router. In a Pages Router app it starts with an empty param set on the server (no access to next/router), which causes hydration mismatches.

2. Use the hooks in your components

import { useUrlParam } from 'next-url-state';

const SearchComponent = () => {
  const [query, setQuery] = useUrlParam('q');

  return (
    <input
      value={query || ''}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
    />
  );
}

App Router Setup

1. Create a client provider component (if only this provider is needed proceed with step 2)

// app/providers.tsx
'use client';

import { UrlParamsProvider } from 'next-url-state';

export const Providers = ({ children }: { children: React.ReactNode }) => {
  return <UrlParamsProvider>{children}</UrlParamsProvider>;
}

2. Use it in your root layout

// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {/* UrlParamsProvider can be used directly if no other provider is needed */}
        <Providers>{children}</Providers> 
      </body>
    </html>
  );
}

3. Use the hooks in your components

// app/search/page.tsx
'use client';

import { useUrlParam } from 'next-url-state';

export default function SearchPage() {
  const [query, setQuery] = useUrlParam('q');

  return (
    <input
      value={query || ''}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
    />
  );
}

Note: Components using these hooks must be client components ('use client')

React Server Components

For React Server Components, use the createRscAdapter function to read URL parameters (no writing):

// app/products/page.tsx (Server Component)
import { createRscAdapter } from 'next-url-state/rsc';

interface PageProps {
  searchParams: Promise<Record<string, string | string[]>>;
}

export default async function ProductsPage({ searchParams }: PageProps) {
  const params = await searchParams;
  const adapter = createRscAdapter(
    '/products',
    new URLSearchParams(params as Record<string, string>)
  );

  const currentPath = adapter.getCurrentPath();
  // → "/products?category=electronics&sort=price"

  // Note: adapter.updateUrl() is a no-op in RSC
  // For URL updates, use a Client Component

  return (
    <div>
      <p>Current path: {currentPath}</p>
      {/* Pass data to Client Components for interactivity */}
    </div>
  );
}

Note: updateUrl() returns false and logs a warning in development mode, since URL updates require client-side JavaScript.

Migration Guide

From Next.js Pages Router (the /pages directory)

Before — manual state sync, boilerplate, and race-condition-prone:

import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';

const SearchPage = () => {
  const router = useRouter();
  const [search, setSearch] = useState((router.query.q as string) ?? '');

  useEffect(() => {
    setSearch((router.query.q as string) ?? '');
  }, [router.query.q]);

  const handleChange = (value: string) => {
    setSearch(value);
    router.replace(
      { query: { ...router.query, q: value } },
      undefined,
      { shallow: true }
    );
  };
};

After — one line, no boilerplate:

import { useUrlParam } from 'next-url-state';

const SearchPage = () => {
  const [search, setSearch] = useUrlParam('q');
};

Provider change in _app.tsx:

// Before
import { useRouter } from 'next/router'; // no provider needed, but lots of manual work

// After
import { UrlParamsPagesRouterProvider } from 'next-url-state';

const MyApp = ({ Component, pageProps }) => (
  <UrlParamsPagesRouterProvider>
    <Component {...pageProps} />
  </UrlParamsPagesRouterProvider>
);

Note: Use UrlParamsPagesRouterProvider (not UrlParamsProvider) in Pages Router apps to avoid hydration mismatches. It reads router.asPath automatically so URL params are available during SSR.


From Next.js App Router (the /app directory)

Before — verbose URL construction on every update:

'use client';
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
import { useCallback } from 'react';

const SearchPage = () => {
  const searchParams = useSearchParams();
  const router = useRouter();
  const pathname = usePathname();

  const search = searchParams.get('q');

  const setSearch = useCallback(
    (value: string) => {
      const params = new URLSearchParams(searchParams.toString());
      if (value) params.set('q', value);
      else params.delete('q');
      router.replace(`${pathname}?${params.toString()}`);
    },
    [searchParams, router, pathname]
  );
};

After:

'use client';
import { useUrlParam } from 'next-url-state';

const SearchPage = () => {
  const [search, setSearch] = useUrlParam('q');
};

Provider change in layout.tsx:

// Before — no provider, but each component wires up routing manually

// After
import { UrlParamsProvider } from 'next-url-state';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <UrlParamsProvider>{children}</UrlParamsProvider>
      </body>
    </html>
  );
}

From use-query-params

Provider — simpler setup, no adapter required:

// Before
import { QueryParamProvider } from 'use-query-params';
import { NextAdapter } from 'next-query-params';

<QueryParamProvider adapter={NextAdapter}>
  <Component {...pageProps} />
</QueryParamProvider>

// After (Pages Router)
import { UrlParamsPagesRouterProvider } from 'next-url-state';

<UrlParamsPagesRouterProvider>
  <Component {...pageProps} />
</UrlParamsPagesRouterProvider>

Hook equivalents:

| use-query-params | next-url-state | |---|---| | useQueryParam('q', StringParam) | useUrlParam('q') | | useQueryParam('page', NumberParam) | useUrlParam<number>('page', { parse: (v) => parseInt(v ?? '1', 10), serialize: String }) | | useQueryParam('flag', BooleanParam) | useUrlParam<boolean>('flag', { parse: (v) => v === 'true', serialize: (v) => v ? 'true' : undefined }) | | useQueryParam('tags', ArrayParam) | useUrlParamArray('tags') | | useQueryParams({ q: StringParam, page: NumberParam }) | useUrlParams(['q', 'page']) |

Example side-by-side:

// Before
import { useQueryParam, useQueryParams, StringParam, NumberParam, ArrayParam } from 'use-query-params';

const [search, setSearch] = useQueryParam('q', StringParam);
const [page, setPage] = useQueryParam('page', NumberParam);
const [tags, setTags] = useQueryParam('tags', ArrayParam);
const [{ q, page }, setQuery] = useQueryParams({ q: StringParam, page: NumberParam });

// After
import { useUrlParam, useUrlParamArray, useUrlParams } from 'next-url-state';

const [search, setSearch] = useUrlParam('q');
const [page, setPage] = useUrlParam<number>('page', { parse: (v) => parseInt(v ?? '1', 10), serialize: String });
const [tags, setTags] = useUrlParamArray('tags');
const [{ q, page }, setQuery] = useUrlParams(['q', 'page']);

API Reference

useUrlParam

Hook for managing a single URL parameter.

const [value, setValue] = useUrlParam<T>(paramName, options?);

Examples:

// Simple string parameter
const [name, setName] = useUrlParam('name');

// With type parsing (number)
const [age, setAge] = useUrlParam<number>('age', {
  parse: (value) => (value ? parseInt(value, 10) : undefined),
  serialize: (value) => value?.toString(),
});

// Boolean parameter
const [enabled, setEnabled] = useUrlParam<boolean>('enabled', {
  parse: (value) => value === 'true',
  serialize: (value) => (value ? 'true' : undefined),
});

// With history entry (creates browser history entry)
await setName('John', { historyEntry: true });

// Without shallow routing (triggers data fetching)
await setName('John', { shallow: false });

useUrlParamArray

Hook for managing URL parameters with multiple values.

const [values, setValues] = useUrlParamArray<T>(paramName, options?);

Example:

// Multiple tags: /page?tag=react&tag=nextjs&tag=typescript
// RECOMMENDED: Always use a parser to ensure you get an array
const [tags, setTags] = useUrlParamArray<string[]>('tag', {
  parse: (value) => {
    if (!value) return [];
    if (Array.isArray(value)) return value;
    return [value]; // Single value becomes array
  },
});

// Now tags is always string[] (never undefined or string)
setTags([...tags, 'new-tag']); // ✅ Safe to use array methods

// Simple usage (may return string | string[] | undefined)
const [tags, setTags] = useUrlParamArray('tag');
// tags could be: undefined, 'single', or ['multiple', 'values']

💡 Best Practice: Always provide a parse function to ensure consistent array behavior and avoid .map() errors.

useUrlParams

Hook for managing multiple URL parameters at once.

const [params, setParams] = useUrlParams<Keys>(keys?);

Examples:

// Subscribe to all URL parameters
const [params, setParams] = useUrlParams();
// params = { q: 'search', page: '1', sort: 'date' }

// Subscribe to specific parameters only
const [params, setParams] = useUrlParams(['q', 'page']);
// Only re-renders when 'q' or 'page' changes

// Update multiple parameters at once
setParams({ q: 'new search', page: '1' });

useUrlParamsArray

Similar to useUrlParams but returns arrays for each param.

const [params, setParams] = useUrlParamsArray<Keys>(keys?);

Example:

const [params, setParams] = useUrlParamsArray(['tags', 'categories']);
// params = { tags: ['react', 'nextjs'], categories: ['web'] }

useUrlParamValue

Read-only hook for getting a URL parameter value without setter.

const value = useUrlParamValue<T>(paramName, options?);

Example:

const currentPage = useUrlParamValue<number>('page', {
  parse: (value) => (value ? parseInt(value, 10) : 1),
});

Advanced Usage

Custom Types with Parsing and Serialization

interface Filters {
  minPrice: number;
  maxPrice: number;
  category: string;
}

const [filters, setFilters] = useUrlParam<Filters>('filters', {
  parse: (value) => {
    if (!value) return { minPrice: 0, maxPrice: 1000, category: 'all' };
    return JSON.parse(value);
  },
  serialize: (filters) => JSON.stringify(filters),
});

Pagination Example

const PaginatedList = () => {
  const [page, setPage] = useUrlParam<number>('page', {
    parse: (value) => (value ? parseInt(value, 10) : 1),
    serialize: (value) => value?.toString(),
  });

  const [itemsPerPage] = useUrlParam<number>('limit', {
    parse: (value) => (value ? parseInt(value, 10) : 10),
    serialize: (value) => value?.toString(),
  });

  return (
    <div>
      <div>Page {page}</div>
      <button onClick={() => setPage(page - 1)} disabled={page <= 1}>
        Previous
      </button>
      <button onClick={() => setPage(page + 1)}>
        Next
      </button>
    </div>
  );
}

Search with Filters

const ProductSearch = () => {
  const [search, setSearch] = useUrlParam('q');
  const [categories, setCategories] = useUrlParamArray('category');
  const [priceRange, setPriceRange] = useUrlParam('price');

  const [allFilters, setAllFilters] = useUrlParams([
    'q',
    'category',
    'price',
    'sort',
  ]);

  const clearFilters = () => {
    setAllFilters({ q: undefined, category: undefined, price: undefined });
  };

  return (
    <div>
      <input
        value={search || ''}
        onChange={(e) => setSearch(e.target.value)}
      />
      <button onClick={clearFilters}>Clear Filters</button>
    </div>
  );
}

Options

UrlChangeOptions

Options that can be passed to setter functions:

interface UrlChangeOptions {
  /**
   * Shallow routing - doesn't run data fetching methods
   * @default true
   */
  shallow?: boolean;

  /**
   * Create a browser history entry
   * @default false
   */
  historyEntry?: boolean;
}

Example:

// Create history entry (browser back button will undo this)
await setQuery('search term', { historyEntry: true });

// Trigger data fetching (getServerSideProps, etc.)
await setQuery('search term', { shallow: false });

How It Works

This library provides optimistic updates with URL synchronization:

  1. Immediate UI updates - When you call a setter, the UI updates immediately
  2. URL synchronization - The URL is updated in the background using Next.js Router (next-router or App router)
  3. Smart batching - Multiple updates within the debounce window are batched into a single URL change
  4. Minimal re-renders - Only components subscribed to changed parameters re-render

Configuration

Library defaults are defined in src/config.ts and can be adjusted to tune behavior:

| Constant | Default | Description | |---|---|---| | HISTORY_DEBOUNCE_MS | 250 | Minimum interval (ms) between URL updates that create separate browser history entries. Rapid changes within this window are coalesced into a single entry, preventing the back button from stepping through every keystroke. |

// src/config.ts
export const HISTORY_DEBOUNCE_MS = 250;

Note for contributors: This is a compile-time constant. To change the debounce for your own needs, clone the repo, edit src/config.ts, and build the package locally. End users of the published package cannot configure this at runtime — if you need runtime configurability, please open an issue.

TypeScript Support

Full TypeScript support with type inference:

// Type is inferred as string | undefined
const [name, setName] = useUrlParam('name');

// Explicit typing
const [count, setCount] = useUrlParam<number>('count', {
  parse: (v) => parseInt(v || '0', 10),
  serialize: (v) => v.toString(),
});

// Multiple params with specific keys
const [params, setParams] = useUrlParams(['search', 'page'] as const);
// params type: { search: string | undefined; page: string | undefined }

Compatibility

  • Next.js: >= 12.0.0 (Pages Router and App Router)
  • React: >= 18.0.0

Router Support

This library supports both Next.js routing systems:

✅ Pages Router (next/router)

  • Fully supported with all features
  • Shallow routing enabled by default
  • Use UrlParamsPagesRouterProvider in pages/_app.tsx — it reads router.asPath from next/router and seeds the provider with the correct path during SSR. This mirrors exactly what Next.js itself does: router.asPath is serialized into __NEXT_DATA__ and available on both server and client, so URL params are consistent across SSR and hydration with zero configuration.
  • UrlParamsProvider is intended for App Router — don't use it in Pages Router apps.

✅ App Router (next/navigation)

  • Fully supported with all features
  • Uses UrlParamsProvider in app/layout.tsx — internally wraps useSearchParams() from next/navigation and applies a mounted guard to prevent hydration mismatches. App Router client components are always ready on the client, so no asPath seed is needed.
  • Note: App Router doesn't support shallow routing (handled gracefully)

✅ React Server Components (RSC)

  • Read-only access to URL parameters
  • Use createRscAdapter from next-url-state/rsc for Server Components
  • Setter is a no-op (URL updates require client-side JavaScript)

Summary: which provider to use?

| App type | Provider | Why | |---|---|---| | Pages Router (/pages) | UrlParamsPagesRouterProvider | Reads router.asPath on server — no hydration mismatch | | App Router (/app) | UrlParamsProvider | Wraps useSearchParams() with a mounted guard | | Both routers — hooks | useUrlParam, useUrlParams, … | Identical API regardless of router |

See the examples folder for working demos of all supported router types:

FAQ / Troubleshooting

Why not just use useSearchParams() directly?

useSearchParams (App Router) and router.query (Pages Router) require you to wire up reading, writing, and URL construction yourself every single time. For a single param that means three hooks, manual URLSearchParams construction, careful spreading to preserve other params, and no batching. next-url-state reduces all of that to one line that behaves like useState.


Does this work with SSR / Server-Side Rendering?

Yes — in two ways:

  • Pages Router: use UrlParamsPagesRouterProvider in pages/_app.tsx. It reads router.asPath from next/router on the server, so URL params are seeded correctly during SSR. The server and client render identically — no hydration mismatch.
  • App Router: use UrlParamsProvider in app/layout.tsx. It wraps useSearchParams() with a mounted guard, which is the standard hydration-safe pattern for App Router client components.
  • React Server Components: use createRscAdapter from next-url-state/rsc for read-only access. Setters are no-ops in RSC since URL updates require client-side JavaScript.

I'm getting a React hydration mismatch — server and client render differently for components that read URL params.

This happens in Pages Router apps when UrlParamsProvider is used instead of UrlParamsPagesRouterProvider. UrlParamsProvider cannot access next/router on the server, so it starts with empty URL params — meaning a component that opens an accordion when ?section=true is present will render closed on the server but open on the client.

Fix: replace UrlParamsProvider with UrlParamsPagesRouterProvider in pages/_app.tsx:

// Before
import { UrlParamsProvider } from 'next-url-state';
// ...
<UrlParamsProvider>{children}</UrlParamsProvider>

// After
import { UrlParamsPagesRouterProvider } from 'next-url-state';
// ...
<UrlParamsPagesRouterProvider>{children}</UrlParamsPagesRouterProvider>

How does batching work exactly?

Every setter call updates the UI immediately (optimistic update), but the actual URL write is debounced. All setter calls that happen within the HISTORY_DEBOUNCE_MS window (default 250 ms) are merged into a single router.replace / router.push call. This means typing into a search input doesn't flood the browser history with one entry per keystroke — only a single history entry is created once the user pauses. The debounce is configurable.


Does batching work across multiple components?

Yes. All hooks share the same internal store via UrlParamsProvider. Updates from different components within the same debounce window are merged into one URL change.


Why do I need UrlParamsProvider?

The provider creates the shared reactive store that all hooks read from and write to. Without it, hooks can't share state between components or batch their updates together. If you forget it, hooks will throw an error pointing you to add the provider.


What happens when I set a value to undefined?

The parameter is removed from the URL entirely, keeping it clean. For example, setSearch(undefined) turns ?q=hello&page=2 into ?page=2.


Does this work with the browser's Back / Forward buttons?

Yes. When historyEntry: true is passed to a setter, a new browser history entry is created and the back button will undo that change. By default (historyEntry: false) the URL is replaced without adding a history entry, which is the right default for things like search inputs.


Can I use this outside of Next.js?

No. The library depends on Next.js routing APIs (next/router for the Pages Router, next/navigation for the App Router) and is not designed for plain React or other frameworks.


How do I set up next-url-state in tests (Jest / Vitest) for a Pages Router project?

In a jsdom test environment, window exists but window.__NEXT_DATA__ is not set. Without it, the library cannot detect that it is running inside a Pages Router app and falls back to the App Router adapter, which causes errors like:

Error: invariant expected app router to be mounted

Add the following to your test setup file (e.g. vitest.setup.ts or jest.setup.ts):

if (typeof window !== "undefined") {
  // Signal Pages Router to next-url-state
  window.__NEXT_DATA__ ??= {
    props: {},
    page: "/",
    query: {},
    buildId: "test",
  };
}

This mirrors what Next.js sets in production and lets the library pick the correct Pages Router adapter. No equivalent setup is needed for App Router projects.

Contributing

Contributions are welcome!

The contributing guide helps you get started with setting up the development environment and explains the development workflow.

License

next-url-state is licensed under the MIT License.

Acknowledgments

This library was inspired by the need for better URL state management in Next.js applications.