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

use-url-sync-state

v1.0.0

Published

A React hook that syncs state to URL search params — making it bookmarkable, shareable, and back-button friendly.

Readme

use-url-sync-state

A React hook that syncs state to URL search params — making your UI state bookmarkable, shareable, and back-button friendly.

npm version bundle size TypeScript


The Problem

You build a dashboard with filters — search, status, page number. The user sets everything up perfectly, copies the URL, and sends it to a teammate. The teammate opens it and sees... the default state. All filters gone.

Or the user hits the back button and loses their filters entirely.

useUrlState fixes this — one hook, works for a single field or many.


Installation

npm install use-url-sync-state

Basic Usage (single field)

import { useUrlState } from "use-url-sync-state";

function SearchBox() {
  const [state, setState] = useUrlState({ search: "" });

  return (
    <input
      value={state.search}
      onChange={(e) => setState({ search: e.target.value })}
      placeholder="Search products..."
    />
  );
}

Multiple Fields (recommended for forms/filters)

Pass an object of defaults and update one or many fields at a time. Every update — whether it touches one field or several — writes the URL in a single change.

import { useUrlState } from "use-url-sync-state";

function ProductsPage() {
  const [state, setState] = useUrlState({
    search: "",
    page: 1,
    status: "all",
  });

  return (
    <div>
      <input
        value={state.search}
        onChange={(e) => setState({ search: e.target.value })}
        placeholder="Search products..."
      />

      <select
        value={state.status}
        onChange={(e) => setState({ status: e.target.value })}
      >
        <option value="all">All</option>
        <option value="active">Active</option>
        <option value="inactive">Inactive</option>
      </select>

      <Pagination
        page={state.page}
        onPageChange={(page) => setState({ page })}
      />

      {/* Update multiple fields in ONE url change */}
      <button
        onClick={() =>
          setState({ search: "shoes", status: "active", page: 1 })
        }
      >
        Apply Quick Filter
      </button>
    </div>
  );
}

Resulting URL as the user interacts:

/products?search=shoes&status=active&page=2

That URL is now shareable and bookmarkable. The back button works. Refresh keeps the state.


API

const [state, setState] = useUrlState(defaults, serializers?);

Parameters

| Parameter | Type | Description | |---|---|---| | defaults | T extends Record<string, unknown> | An object defining every field and its default value — even for a single field, pass { search: "" } | | serializers | { [key]: Serializer<T[key]> } | Optional — only needed for types the built-in default can't handle (e.g. Date) |

Returns

| Value | Type | Description | |---|---|---| | state | T | Current state, read from the URL (or defaults if absent) | | setState | (updates: Partial<T>) => void | Merges updates into state and writes all changed fields to the URL in one update |

Built-in default serialization (no config needed)

| Value type | URL format | Example | |---|---|---| | string | plain text | ?search=shoes | | number | plain number | ?page=2 | | boolean | true / false | ?inStock=true | | array of primitives | comma-separated | ?tags=sale,new,trending | | object / array of objects | JSON | ?filters={"category":"shoes"} |

Commas in the URL are kept literal (not encoded as %2C) for readability.


Custom Serializer

For values the default can't handle cleanly — like Date:

const dateSerializer = {
  parse: (value: string) => new Date(value),
  stringify: (value: Date) => value.toISOString().split("T")[0],
};

const [state, setState] = useUrlState(
  { date: new Date() },
  { date: dateSerializer }
);

Why One Object Instead of One Hook Call Per Field

An earlier version of this package had two hooks — one for a single field, one for many. That created a trap: updating several fields in the same event handler with separate hook instances can cause them to overwrite each other, because each instance reads window.location independently and doesn't know about the others' pending changes.

// ⚠️ Two independent hook calls — updating both in the same handler
// can lose data, since each setValue reads the URL separately.
const [search, setSearch] = useSomeOtherLibrary("search", "");
const [status, setStatus] = useSomeOtherLibrary("status", "all");

function applyFilter() {
  setSearch("shoes");   // writes ?search=shoes
  setStatus("active");  // may not see the line above yet — risk of overwrite
}

useUrlState avoids this entirely by treating all fields as one state object and writing the whole URL in a single pushState call — so a multi-field update can never partially overwrite itself, whether you're updating one field or five.


Behaviour Notes

  • Back/forward button — listens to popstate, state updates on navigation
  • Clean URLs — when a value equals its default, the param is removed from the URL automatically
  • Malformed URLs — if a param can't be parsed (e.g. manually edited to garbage), the hook falls back to the default for that field instead of crashing

SSR Behaviour & Limitations

This hook is crash-safe on the server (it checks typeof window and never throws during SSR), but it is not SSR-hydrated. This distinction matters:

  • On the server, state always renders as defaults, since there's no window.location to read from.
  • On the client, immediately after mount, the hook re-reads the real URL and updates state.
  • This causes a brief flash from default → actual value on first render. In frameworks with strict hydration checks (e.g. Next.js App Router), this can trigger a hydration mismatch warning if the rendered output differs between server and client.

Why this happens: truly SSR-correct URL state requires reading the request URL on the server, which is framework-specific (Next.js's searchParams prop, Remix loaders, etc.). A framework-agnostic hook has no generic way to access that.

If you're on Next.js App Router and need zero-flash, hydration-safe URL state, pass the server's searchParams in as the initial value rather than relying solely on this hook, or use a framework-specific solution like nuqs.

For client-rendered apps (Vite, CRA, client components without SSR), this limitation doesn't apply — state is correct from the very first render.


When to Use This

✅ Dashboard filters (search, status, category) ✅ Pagination ✅ Tab state you want to be linkable ✅ Multi-field forms/filters that should be shareable as a URL ✅ Any UI state that should survive a page refresh


License

MIT © Danish