Package Exports
- state-in-url
- state-in-url/encodeState
- state-in-url/encoder
- state-in-url/next
- state-in-url/react-router
- state-in-url/useSharedState
- state-in-url/useUrlEncode
- state-in-url/useUrlStateBase
- state-in-url/utils
Readme

Store any complex state in query parameters, URL synchronization (aka deep links) made easy. Share state between any unrelated components. With support of static types validation and server side rendering.
Don't hesitate to open an issue if you found a bug, or for requesting features
Add a ⭐️ and follow me to support the project!
Will appreciate you feedback/opinion on discussions
Share if it useful for you. FB LinkedIn X.com VK
Why use state-in-url
?
Store any user state in query parameters; imagine JSON in a browser URL, while keeping types and structure of data. Dead simple, fast, and with static Typescript validation. Deep links, aka URL synchronization, made easy.
Contains useUrlState
hook for Next.js and react-router, and helpers for anything else on JS.
Since modern browsers support huge URLs and users don't care about query strings (it is a select all and copy/past workflow).
Time to use query string for state management, as it was originally intended. This library does all mundane stuff for you.
This library is a good alternative for NUQS.
Use cases
- 💾 Store unsaved user forms or page filters in URL
- 🧠 Just sync data between unrelated client components without touching URI
- 🔗 Shareable URLs with application state (Deep linking, URL state synchronization)
- 🔄 Easy state persistence across page reloads
Features
- 🧩 Simple: No providers, reducers, boilerplate or new concepts, API similar to
React.useState
- 📘 Typescript validation: State is just an object, automatic static validation in IDE/tests according to Typescript definition
- ✨ Complex data: Nested objects, dates and arrays, works same as JSON, but in URL
- 🔵 Server Side Rendering: Can use it in Server Components, Next.js 14 and 15 are supported
- ⚙ Well tested: Unit tests and Playwright tests for Chrome/Firefox/Safari
- ⚡ Fast: Minimal rerenders, less than 1ms to encode and decode big object
- 🪶 Lightweight: Zero dependencies, library less than 2KB
- 👍 DX: Good developer experience, documentation, JSDoc comments, and examples
- ⚛️ Framework Flexibility: Hooks for
Next.js
andreact-router
, helpers to use it with other frameworks or pure JS - 📃 Permissive license: MIT
Table of content
installation
1. Install package
# npm
npm install --save state-in-url
# yarn
yarn add state-in-url
# pnpm
pnpm add state-in-url
2. Edit tsconfig.json
In tsconfig.json
in compilerOptions
set "moduleResolution": "Bundler"
, or"moduleResolution": "Node16"
, or "moduleResolution": "NodeNext"
.
Possibly need to set "module": "ES2022"
, or "module": "ESNext"
useUrlState
Main hook that takes initial state as parameter and returns state object, callback to update url, and callback to update only state.
All components that use the same state
object are automatically synchronized.
useUrlState hook for Next.js
Usage examples
Basic
Define state shape with default values
// userState.ts // Only parameters with value different from default will go to the url. export const userState: UserState = { name: '', age: 0 } type UserState = { name: string, age: number }
Import it and use
'use client'
import { useUrlState } from 'state-in-url/next';
import { userState } from './userState';
function MyComponent() {
// can pass `replace` arg, it's control will `setUrl` will use `rounter.push` or `router.replace`, default replace=true
// can pass `searchParams` from server components
const { urlState, setUrl, setState } = useUrlState({ defaultState: userState });
// won't let you to accidently mutate state directly, requires TS
// urlState.name = 'John' // <- error
return (
<div>
<input value={urlState.name}
// same api as React.useState, e.g. setUrl(currVal => currVal + 1)
onChange={(ev) => setUrl({ name: ev.target.value }) }
/>
<input value={urlState.age}
onChange={(ev) => setUrl({ age: +ev.target.value }) }
/>
<input value={urlState.name}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
// Can update state immediately but sync change to url as needed
onBlur={() => setUrl()}
/>
<button onClick={() => setUrl(userState)}>
Reset
</button>
</div>
)
}
With complex state shape
Example
export 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 } }[];
};
'use client'
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
function TagsComponent() {
// `urlState` will infer from Form type!
const { urlState, setUrl } = useUrlState({ defaultState: form });
const onChangeTags = React.useCallback(
(tag: (typeof tags)[number]) => {
setUrl((curr) => ({
...curr,
tags: curr.tags.find((t) => t.id === tag.id)
? curr.tags.filter((t) => t.id !== tag.id)
: curr.tags.concat(tag),
}));
},
[setUrl],
);
return (
<div>
<Field text="Tags">
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Tag
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
/>
))}
</div>
</Field>
</div>
);
}
const tags = [
{
id: '1',
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
},
{
id: '2',
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
},
{
id: '3',
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
},
];
Update state only and sync to URL manually
Example
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
React.useEffect(() => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
// will compare state by content not by reference and fire update only for new values
setUrl(urlState);
}, 500);
return () => {
clearTimeout(timer.current);
};
}, [urlState, setUrl]);
Syncing state onBlur
will be more aligned with real world usage.
<input onBlur={() => updateUrl()} .../>
With server side rendering
Example
export default async function Home({ searchParams }: { searchParams: object }) {
return (
<Form searchParams={searchParams} />
)
}
// Form.tsx
'use client'
import React from 'react';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
const Form = ({ searchParams }: { searchParams: object }) => {
const { urlState, setState, setUrl } = useUrlState({ defaultState: form, searchParams });
}
Using hook in layout
component
Example
That a tricky part, since nextjs with app router doesn't allow to access searchParams from server side. There is workaround with using middleware, but it isn't pretty and can stop working after nextjs update.// add to appropriate `layout.tsc`
export const runtime = 'edge';
// middleware.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const url = request.url?.includes('_next') ? null : request.url;
const sp = url?.split?.('?')?.[1] || '';
const response = NextResponse.next();
if (url !== null) {
response.headers.set('searchParams', sp);
}
return response;
}
// Target layout component
import { headers } from 'next/headers';
import { decodeState } from 'state-in-url/encodeState';
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const sp = headers().get('searchParams') || '';
return (
<div>
<Comp1 searchParams={decodeState(sp, stateShape)} />
{children}
</div>
);
}
With arbitrary state shape (not recommended)
Example
'use client'
import { useUrlState } from 'state-in-url/next';
const someObj = {};
function SettingsComponent() {
const { urlState, setUrl, setState } = useUrlState<object>(someObj);
}
useUrlState hook for React-Router
API is same as for Next.js version, except can pass options from NavigateOptions type.
Example
export 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 } }[];
};
import { useUrlState } from 'state-in-url/react-router';
import { form } from './form';
function TagsComponent() {
const { urlState, setUrl, setState } = useUrlState({ defaultState: form });
const onChangeTags = React.useCallback(
(tag: (typeof tags)[number]) => {
setUrl((curr) => ({
...curr,
tags: curr.tags.find((t) => t.id === tag.id)
? curr.tags.filter((t) => t.id !== tag.id)
: curr.tags.concat(tag),
}));
},
[setUrl],
);
return (
<div>
<Field text="Tags">
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Tag
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
/>
))}
</div>
</Field>
<input value={urlState.name}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
// Can update state immediately but sync change to url as needed
onBlur={() => setUrl()}
/>
</div>
);
}
const tags = [
{
id: '1',
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
},
{
id: '2',
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
},
{
id: '3',
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
},
];
Other hooks and helpers
useUrlStateBase
hook for others routers
Hooks to create your own useUrlState
hooks with other routers, e.g. react-router or tanstack router.
useSharedState
hook for React.js
Hook to share state between any React components, tested with Next.js and Vite.
'use client'
import { useSharedState } from 'state-in-url';
export const someState = { name: '' };
function SettingsComponent() {
const { state, setState } = useSharedState(someState);
}
useUrlEncode
hook for React.js
encodeState
and decodeState
helpers
encode
and decode
helpers
Best Practices
- Define your state shape as a constant
- Use TypeScript for enhanced type safety and autocomplete
- Avoid storing sensitive information in URL parameters (SSN, API keys etc)
- Use this extension for readable TS errors
Can create state hooks for slices of state, and reuse them across application. For example:
type UserState = {
name: string;
age: number;
other: { id: string, value: number }[]
};
const userState = {
name: '',
age: 0,
other: [],
};
export const useUserState = () => {
const { urlState, setUrl } = useUrlState({ defaultState: userState });
return { userState: urlState, setUserState: setUrl };;
}
Gothas
- Can pass only serializable values,
Function
,BigInt
orSymbol
won't work, probably things likeArrayBuffer
neither. Everything that can be serialized to JSON will work. - Vercel servers limit size of headers (query string and other stuff) to 14KB, so keep your URL state under ~5000 words. https://vercel.com/docs/errors/URL_TOO_LONG
- Tested with
next.js
14/15 with app router, no plans to support pages.
Other
Contribute and/or run locally
See Contributing doc
Roadmap
- hook for
Next.js
- hook for 'react-router`
- hook for 'remix`
- hook for store state in hash ?
Contact & Support
- Create a GitHub issue for bug reports, feature requests, or questions
Changelog
License
This project is licensed under the MIT license.