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';
export const ComponentA = () => {
// see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
// useHistory: false to update sp on server component
const { urlState, setState, setUrl } = useUrlState(form, { useHistory: true });
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 { useUrlState } from 'state-in-url/next';
import { form } from './form';
// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
const { urlState } = useUrlState(form, { 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';
const form: Form={
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: {id: string; value: {text: string; time: Date } }[];
};
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.
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

