Typed state, living in the URL

useUrlState is React state that writes itself to the query string. Objects, arrays and dates keep their types, every state is a shareable link, and it survives reloads — no providers, no boilerplate.

  • ~2 KB gzipped
  • zero dependencies
  • TypeScript-first
  • Next.js / react-router / Remix
  • MIT
npm i state-in-url

useUrlState — live with next.js

Type below — watch the URL light up

First client component
Other client component

Reads from URL — no props, no context, types and structure are preserved

{
name: string
age: undefinedundefined
agree_to_terms: falseboolean
tags: []
}

Same API, three routers

Quick start

1. Define the state
state
export const form: Form = {
  name: '',
  age: undefined,
  agree_to_terms: false,
  tags: [],
};

// use `Type` not `Interface`!
type Form = {
  name: string;
  age?: number;
  agree_to_terms: boolean;
  tags: { id: string; value: { text: string; time: Date } }[];
};
2. Wrap it in a reusable hook
useFormState
'use client';

import { useUrlState } from 'state-in-url/next';
import { form } from './form';

// One hook per feature - the whole API for this state
// "searchParams" only needed to pass params from Server Components
export const useFormState = (searchParams?: object) =>
  useUrlState(form, { searchParams });
3. Use it in any components — they all share it
ComponentA
'use client';

import { useFormState } from './useFormState';

export const ComponentA = () => {
  // see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
  const { urlState, setState, setUrl } = useFormState(); 

  return <>
    <input
      id="name"
      value={urlState.name} 
      onChange={(ev) => setUrl({ name: ev.target.value })}
      />
    // OR can update state immediately but sync change to url as needed
    <input
      value={urlState.name}
      onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
      onBlur={() => setUrl()}
    />
    <button onClick={() => setUrl((curr, initial) => initial)}>
      Reset
    </button>
    </>
};
ComponentB
'use client';
import { useFormState } from './useFormState';

// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
  // same state as ComponentA - no props, no context
  const { urlState } = useFormState(searchParams);

// will be defaultValue from `form` if not in url, no need to check

  return <div>name: {urlState.name}</div>
};
4. Grow the hook when you need more
useFormState - extended
'use client';

import React from 'react';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';

export const useFormState = ({ searchParams }: { searchParams?: object }) => {
  const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
    searchParams,
  });

  // first navigation will push new history entry
  // all following will just replace that entry
  // this way will have history with only 2 entries - ['/url', '/url?key=param']

  const replace = React.useRef(false);
  const setUrl = React.useCallback((
      state: Parameters<typeof setUrlBase>[0],
      opts?: Parameters<typeof setUrlBase>[1]
    ) => {
      setUrlBase(state, { replace: replace.current, ...opts });
      replace.current = true;
  }, [setUrlBase]);

  return { urlState, setUrl, resetUrl: reset };
};

Using an AI coding agent?

Agents get the same two things wrong here, every time. They type the state shape with interface, which the JSONCompatible constraint rejects outright. And they build the default-state object inside the component, which breaks sharing silently — it is keyed by object identity, so nothing errors, the two components simply stop seeing each other.

So the package ships six SKILL.md files. Your agent loads one on demand through TanStack Intent, and they are versioned with the library rather than with this page.

npx @tanstack/intent@latest install

Run once in a project that already has state-in-url installed. Your agent then finds the skills in node_modules/state-in-url/skills/.

  • feature-state-hookDefining state, and wrapping useUrlState in a feature-scoped hook
  • input-handlingText inputs, sliders, anything that changes fast
  • nextjs-ssrApp Router: searchParams forwarding, Proxy for layouts
  • react-router-remix-setupReact Router v6/v7 or Remix v2 setup
  • form-library-integrationPairing with react-hook-form (or formik)
  • shared-state-no-urluseSharedState — sharing without touching the URL

The sources are on GitHub. An agent that can't load Intent skills should read llms.txt instead — the same rules, condensed into one file.

state-in-url vs nuqs

Searching for a nuqs alternative? Both keep typed state in the query string; they differ in how much you set up and what a value can be.

Whatstate-in-urlnuqs
SetupNone — import the hook and goAdapter component wraps the app
State shapeOne typed object, like React.useStatePer-key values, a parser declared for each
Reuse across componentsWrap the hook once — every component shares the state, no propsExtract your own hook around the parser map
Nested objects and arraysBuilt in — structure and types preservedJSON parser plus your own runtime validator
DatesPreserved automaticallyBuilt-in parser, declared per key
Size, full import~2.9 KB gzipped~6.7 KB gzipped
Runtime dependenciesNoneOne
RoutersNext.js, React Router v6/v7, Remix, plain JS helpersNext.js, React Router, Remix, TanStack Router, plain React

Sizes: whole-library import, esbuild minify + gzip, measured August 2026 against nuqs 2.10.1.

nuqs is a fine library — reach for it when you want each value as its own readable query param, or you are on TanStack Router. Reach for state-in-url when you want a whole typed object in the URL with zero setup.

Read the full comparison — same feature built in both, and how to migrate

Why state-in-url?

URL state libraries exist, but most are either cumbersome to set up or limited in what they can store. state-in-url aims to be the one that just works: an API that mirrors React.useState, with the URL as the store.

Store state without boilerplate, build deep links, and share data between unrelated client components — no provider needed. Structure and types are preserved end to end: a Date goes in, a Date comes out.

Built test-first, with unit and cross-browser e2e suites running on every commit.

Next.js: no Suspense boundary

The hook never calls useSearchParams, so a component using it doesn't need wrapping in Suspense and doesn't opt its page out of prerendering — PPR and cacheComponents included. It reads the URL directly and follows every later change, including a history.pushState from code that knows nothing about it.

Not on Next.js or react-router?

The encodeState / decodeState helpers work with any framework or plain JS — the hooks are a convenience on top.

Check out the GitHub page — a star goes a long way.

Share it with other devs

Uneed Embed Badge