Typed URL state for React & Next.js — like useState
useUrlState is React state that writes itself to the query string. Objects, arrays and dates keep their types, every state is a shareable link, it survives reloads and the back button works — no providers, no Suspense boundary, no boilerplate.
- ~2 KB gzipped
- zero dependencies
- TypeScript-first
- Next.js / react-router / Remix / Astro
- MIT
npm i state-in-urluseUrlState — live with next.js
Type below — watch the URL light up
Reads from URL — no props, no context, types and structure are preserved
{name: stringage: undefinedundefinedagree_to_terms: falsebooleantags: []}
URL state management for Next.js, React Router, Remix and Astro — same API
Quick start
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 } }[];
};'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 });'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>
</>
};'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>
};'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 seven 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 installRun 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 hookinput-handlingText inputs, sliders, anything that changes fastnextjs-ssrApp Router: searchParams forwarding, Proxy for layoutsreact-router-remix-setupReact Router v6/v7 or Remix v2 setupastro-setupAstro islands (React or Preact), or pages with no client frameworkform-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.
| What | state-in-url | nuqs |
|---|---|---|
| Setup | Next.js, React Router v6/v7, Remix, Astro, plain JS helpers | Adapter component wraps the app |
| State shape | One typed object, like React.useState | Per-key values, a parser declared for each |
| Reuse across components | Wrap the hook once — every component shares the state, no props | Extract your own hook around the parser map |
| Nested objects and arrays | Built in — structure and types preserved | JSON parser plus your own runtime validator |
| Dates | Preserved automatically | Built-in parser, declared per key |
| Size, full import | ~2.9 KB gzipped | ~6.7 KB gzipped |
| Runtime dependencies | None | One |
| Routers | Next.js, React Router v6/v7, Remix, plain JS helpers | Next.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
URL state in React — frequently asked questions
- Why keep React state in the URL?
- A URL that holds the state is a shareable link: reload, bookmark or send it and the same filters, tab or page open. Back and forward work for free, and unrelated components can read the same values without a provider. state-in-url does this with one typed object instead of hand-parsed strings.
- What state belongs in the URL?
- Anything a reader might bookmark or share: filters, sorting, pagination, the active tab, a date range, search text. Keep out what is private, huge or purely transient — auth tokens, whether a dialog is open, mouse position. A quick test: would a shared link still make sense with this value in it?
- How do I read and set URL params in React with state-in-url?
- Call useUrlState with a default-state object. urlState holds the current values, already typed; setUrl writes a partial object to the query string; setState updates the state without touching the URL until you flush it. Numbers, booleans, arrays, nested objects and Dates come back as the same types they went in.
- Does URL state survive a page refresh?
- Yes. The state is the query string, so a reload, a bookmark or a link pasted somewhere else restores it. On the Next.js App Router, pass the page’s searchParams prop into the hook so the first server render already shows the right values instead of the defaults.
- Does it work with Next.js Server Components, without a Suspense boundary?
- Yes. The hook never calls useSearchParams, so a component using it needs no Suspense boundary and does not opt the page out of prerendering, PPR included. Server Components read the same state through the searchParams prop; a layout can decode it from a header set in proxy.ts.
- Can I sync react-hook-form or a table library with the URL?
- Yes. Keep the form library as the source of truth, seed it with urlState as the default values, and mirror its changes with setUrl from a change handler or an effect. The same pattern works for TanStack Table state, filter panels and anything else that exposes values and a setter.
- Which frameworks does state-in-url support?
- Next.js 14-16 App Router, React Router v6 and v7, Remix v2 and Astro islands (React or Preact), each through its own entry point. Plain JavaScript and any other framework can use the encodeState and decodeState helpers directly. It is ~2 KB gzipped with zero dependencies.
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

