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-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: []}
Same API, three routers
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 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 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 setupform-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 | None — import the hook and go | 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
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

