Store state in URL as in object, types and structure are preserved, with TS validation. Same API as React.useState, wthout any hasssle or boilerplate. Next.js@14-15, react-router@6-7, and remix@2.
npm install state-in-url
# State in url

!npm bundle size (minified + gzip)

!Tests






URI size limitation, up to 12KB is safe
Add a
and follow me to support the project!
Will appreciate you feedback/opinion on discussions
Share if it useful for you.
X.com
LinkedIn
FB
VK
state-in-url?Store any user state in query parameters; imagine JSON in a browser URL. All of it with keeping types and structure of data, e.g. numbers will be decoded as numbers not strings, dates as dates, etc, objects and arrays supported.
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.
- Store unsaved user forms or page filters in URL
- Sync URL with React state
- 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
- 🧩 Simple: No providers, reducers, boilerplate or new concepts, API similar to React.useState
- 📘 Typescript validation/autocomplete: 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
- ☂ Default values: Giving you default values if parameter not in url
- ⌨ Organized: All possible values defined at start, protect you from getting non existing key
- compatible: Will keep 3rd party query params as is
- flexible: Can use more than 1 state objects on the same page, just use different keys
- Fast: Minimal rerenders, around 1ms to encode and decode big object
- Server Side Rendering: Can use it in Server Components, Next.js 14, 15, and 16 are supported
- Lightweight: Zero dependencies, library less than 2KB
- DX: Good developer experience, documentation, JSDoc comments, and examples
- Framework Flexibility: Hooks for Next.js and react-router, helpers to use it with other frameworks or pure JS
- Well tested: Unit tests and Playwright tests for Chrome/Firefox/Safari
- Permissive license: MIT
- State in url
- Demo
- Why use state-in-url?
- Use cases
- Features
- Table of content
- installation
- 1. Install package
- 2. Edit tsconfig.json
- useUrlState
- useUrlState hook for Next.js
- Usage examples
- Basic
- With server side rendering
- Using hook in layout component
- With arbitrary state shape (not recommended)
- useUrlState hook for Remix.js
- Example
- useUrlState hook for React-Router
- Example
- Recipes
- Custom hook to work with slice of state conveniently
- With complex state shape
- Update state only and sync to URL manually
- Other hooks and helpers
- useUrlStateBase hook for others routers
- useSharedState hook for React.js
- useUrlEncode hook for React.js
- encodeState and decodeState helpers
- encode and decode helpers
- Best Practices
- Gotchas
- Other
- Contribute and/or run locally
- Roadmap
- Contact \& Support
- Changelog
- Mentions
- License
- Personal website
- Inspiration
``sh`npm
npm install --save state-in-urlyarn
yarn add state-in-urlpnpm
pnpm add state-in-url
In tsconfig.json in compilerOptions set "moduleResolution": "Bundler", or"moduleResolution": "Node16", or "moduleResolution": "NodeNext"."module": "ES2022"
Possibly need to set , or "module": "ESNext"
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.
#### Usage examples
##### Basic
1. Define state shape with default values
`typescript
// userState.ts
// Only parameters with value different from default will go to the url.
export const userState: UserState = { name: '', age: 0 }
// use Type not Interface!`
type UserState = { name: string, age: number }
2. Import it and use
`typescript
'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=truesearchParams
// can pass from server components, pass useHistory: false if you need to fetch smt in the server component
const { urlState, setUrl, setState } = useUrlState(userState);
return (
if url empty
// same api as React.useState, e.g. setUrl(currVal => currVal + 1)
onChange={(ev) => setUrl({ name: ev.target.value }) }
/>
onChange={(ev) => setUrl({ age: +ev.target.value }) }
/> onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
// Can update state immediately but sync change to url as needed
onBlur={() => setUrl()}
/>
##### With server side rendering
Example
`typescript
export default async function Home({ searchParams }: { searchParams: object }) {
return (
)
}// 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(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.
`typescript
// 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 (
{children}
);
}
`##### With arbitrary state shape (not recommended)
Example
`typescript
'use client'
import { useUrlState } from 'state-in-url/next';const someObj = {};
function SettingsComponent() {
const { urlState, setUrl, setState } = useUrlState
`$3
API is same as for Next.js version, except can pass options from NavigateOptions type.
#### Example
`typescript
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 } }[];
};
``typescript
import { useUrlState } from 'state-in-url/remix';import { form } from './form';
function TagsComponent() {
const { urlState, setUrl, setState } = useUrlState(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 (
{tags.map((tag) => (
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
/>
))}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
// Can update state immediately but sync change to url as needed
onBlur={() => setUrl()}
/>
);
}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') },
},
];
`$3
API is same as for Next.js version, except can pass options from NavigateOptions type.
#### Example
`typescript
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 } }[];
};
``typescript
import { useUrlState } from 'state-in-url/react-router';
// for react-router v6
// import { useUrlState } from 'state-in-url/react-router6';import { form } from './form';
function TagsComponent() {
const { urlState, setUrl, setState } = useUrlState(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 (
{tags.map((tag) => (
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
/>
))}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
// Can update state immediately but sync change to url as needed
onBlur={() => setUrl()}
/>
);
}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') },
},
];
`Recipes
##### Custom hook to work with slice of state conveniently
Example
`typescript
'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[0],
opts?: Parameters[1]
) => {
setUrlBase(state, { replace: replace.current, ...opts });
replace.current = true;
}, [setUrlBase]);
return { urlState, setUrl, resetUrl: reset };
};
`
##### With complex state shape
Example
`typescript
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 } }[];
};
``typescript
'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(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 (
{tags.map((tag) => (
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
/>
))}
);
}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
`typescript 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.`typescript
updateUrl()} .../>
`Other hooks and helpers
$3
Hooks to create your own
useUrlState hooks with other routers, e.g. react-router or tanstack router.$3
Hook to share state between any React components, tested with Next.js and Vite.
`typescript
'use client'
import { useSharedState } from 'state-in-url';export const someState = { name: '' };
function SettingsComponent() {
const { state, setState } = useSharedState(someState);
}
`$3
$3
$3
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:
`Typescript
type UserState = {
name: string;
age: number;
other: { id: string, value: number }[]
};
const userState = {
name: '',
age: 0,
other: [],
};export const useUserState = () => {
const { urlState, setUrl, reset } = useUrlState(userState);
// other logic
// reset query params when navigating to other page
React.useEffect(() => {
return reset
}, [])
return { userState: urlState, setUserState: setUrl };;
}
`Gotchas
1. Can pass only serializable values,
Function, BigInt or Symbol won't work, probably things like ArrayBuffer neither. Everything that can be serialized to JSON will work.
2. Vercel servers limit size of headers (query string and other stuff) to 14KB, so keep your URL state under ~5000 words.
3. Tested with next.js 14/15/16 with app router, no plans to support pages.Other
$3
See Contributing doc
Roadmap
- [x] hook for
Next.js
- [x] hook for react-router
- [x] hook for remix
- [ ] hook for svelte
- [ ] hook for astro`- Create a GitHub issue for bug reports, feature requests, or questions
- This Week in React 209
- JavaScript Weekly
- This Week in React 240
This project is licensed under the MIT license.