Gathering detailed insights and metrics for state-in-url
Gathering detailed insights and metrics for state-in-url
Gathering detailed insights and metrics for state-in-url
Gathering detailed insights and metrics for state-in-url
Store any user state in query parameters; imagine JSON in a browser URL, while keeping types and structure of data, e.g.numbers will be decoded as numbers not strings. With TS validation. Shared state and URL state sync without any hassle or boilerplate. Supports Next.js@14-15, react-router@6-7, and Remix@2.
npm install state-in-url
Typescript
Module System
Node Version
NPM Version
TypeScript (94.41%)
JavaScript (3.37%)
CSS (1.86%)
HTML (0.36%)
Built with Next.js • Fully responsive • SEO optimized • Open source ready
Total Downloads
3,605,959
Last Day
26
Last Week
2,001
Last Month
7,774
Last Year
3,604,849
MIT License
371 Stars
672 Commits
9 Forks
3 Watchers
1 Branches
4 Contributors
Updated on Sep 02, 2025
Latest Version
6.0.1
Package Id
state-in-url@6.0.1
Unpacked Size
120.59 kB
Size
32.55 kB
File Count
99
NPM Version
10.9.0
Node Version
22.18.0
Published on
Aug 22, 2025
Cumulative downloads
Total Downloads
Last Day
-39.5%
26
Compared to previous day
Last Week
19.3%
2,001
Compared to previous week
Last Month
-73.9%
7,774
Compared to previous month
Last Year
324,661.2%
3,604,849
Compared to previous year
1
60
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.
React.useState
Next.js
and react-router
, helpers to use it with other frameworks or pure JSstate-in-url
?
1# npm 2npm install --save state-in-url 3# yarn 4yarn add state-in-url 5# pnpm 6pnpm add state-in-url
In tsconfig.json
in compilerOptions
set "moduleResolution": "Bundler"
, or"moduleResolution": "Node16"
, or "moduleResolution": "NodeNext"
.
Possibly need to set "module": "ES2022"
, 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.
1// userState.ts 2// Only parameters with value different from default will go to the url. 3export const userState: UserState = { name: '', age: 0 } 4 5// use `Type` not `Interface`! 6type UserState = { name: string, age: number }
1'use client' 2import { useUrlState } from 'state-in-url/next'; 3 4import { userState } from './userState'; 5 6function MyComponent() { 7 // can pass `replace` arg, it's control will `setUrl` will use `rounter.push` or `router.replace`, default replace=true 8 // can pass `searchParams` from server components, pass `useHistory: false` if you need to fetch smt in the server component 9 const { urlState, setUrl, setState } = useUrlState(userState); 10 11 return ( 12 <div> 13 // urlState.name will return default value from `userState` if url empty 14 <input value={urlState.name} 15 // same api as React.useState, e.g. setUrl(currVal => currVal + 1) 16 onChange={(ev) => setUrl({ name: ev.target.value }) } 17 /> 18 <input value={urlState.age} 19 onChange={(ev) => setUrl({ age: +ev.target.value }) } 20 /> 21 22 <input value={urlState.name} 23 onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }} 24 // Can update state immediately but sync change to url as needed 25 onBlur={() => setUrl()} 26 /> 27 28 <button onClick={() => setUrl((_, initial) => initial)}> 29 Reset 30 </button> 31 32 </div> 33 ) 34}
1export default async function Home({ searchParams }: { searchParams: object }) { 2 return ( 3 <Form searchParams={searchParams} /> 4 ) 5} 6 7// Form.tsx 8'use client' 9import React from 'react'; 10import { useUrlState } from 'state-in-url/next'; 11import { form } from './form'; 12 13const Form = ({ searchParams }: { searchParams: object }) => { 14 const { urlState, setState, setUrl } = useUrlState(form, { searchParams }); 15}
layout
component1// add to appropriate `layout.tsc` 2export const runtime = 'edge'; 3 4// middleware.ts 5import type { NextRequest } from 'next/server'; 6import { NextResponse } from 'next/server'; 7 8export function middleware(request: NextRequest) { 9 const url = request.url?.includes('_next') ? null : request.url; 10 const sp = url?.split?.('?')?.[1] || ''; 11 12 const response = NextResponse.next(); 13 14 if (url !== null) { 15 response.headers.set('searchParams', sp); 16 } 17 18 return response; 19} 20 21// Target layout component 22import { headers } from 'next/headers'; 23import { decodeState } from 'state-in-url/encodeState'; 24 25export default async function Layout({ 26 children, 27}: { 28 children: React.ReactNode; 29}) { 30 const sp = headers().get('searchParams') || ''; 31 32 return ( 33 <div> 34 <Comp1 searchParams={decodeState(sp, stateShape)} /> 35 {children} 36 </div> 37 ); 38} 39 40
1'use client' 2import { useUrlState } from 'state-in-url/next'; 3 4const someObj = {}; 5 6function SettingsComponent() { 7 const { urlState, setUrl, setState } = useUrlState<object>(someObj); 8}
API is same as for Next.js version, except can pass options from NavigateOptions type.
1export const form: Form = { 2 name: '', 3 age: undefined, 4 agree_to_terms: false, 5 tags: [], 6}; 7 8type Form = { 9 name: string; 10 age?: number; 11 agree_to_terms: boolean; 12 tags: { id: string; value: { text: string; time: Date } }[]; 13}; 14
1import { useUrlState } from 'state-in-url/remix'; 2 3import { form } from './form'; 4 5function TagsComponent() { 6 const { urlState, setUrl, setState } = useUrlState(form); 7 8 const onChangeTags = React.useCallback( 9 (tag: (typeof tags)[number]) => { 10 setUrl((curr) => ({ 11 ...curr, 12 tags: curr.tags.find((t) => t.id === tag.id) 13 ? curr.tags.filter((t) => t.id !== tag.id) 14 : curr.tags.concat(tag), 15 })); 16 }, 17 [setUrl], 18 ); 19 20 return ( 21 <div> 22 <Field text="Tags"> 23 <div className="flex flex-wrap gap-2"> 24 {tags.map((tag) => ( 25 <Tag 26 active={!!urlState.tags.find((t) => t.id === tag.id)} 27 text={tag.value.text} 28 onClick={() => onChangeTags(tag)} 29 key={tag.id} 30 /> 31 ))} 32 </div> 33 </Field> 34 35 <input value={urlState.name} 36 onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }} 37 // Can update state immediately but sync change to url as needed 38 onBlur={() => setUrl()} 39 /> 40 </div> 41 ); 42} 43 44const tags = [ 45 { 46 id: '1', 47 value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') }, 48 }, 49 { 50 id: '2', 51 value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') }, 52 }, 53 { 54 id: '3', 55 value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') }, 56 }, 57];
API is same as for Next.js version, except can pass options from NavigateOptions type.
1export const form: Form = { 2 name: '', 3 age: undefined, 4 agree_to_terms: false, 5 tags: [], 6}; 7 8type Form = { 9 name: string; 10 age?: number; 11 agree_to_terms: boolean; 12 tags: { id: string; value: { text: string; time: Date } }[]; 13}; 14
1import { useUrlState } from 'state-in-url/react-router'; 2// for react-router v6 3// import { useUrlState } from 'state-in-url/react-router6'; 4 5import { form } from './form'; 6 7function TagsComponent() { 8 const { urlState, setUrl, setState } = useUrlState(form); 9 10 const onChangeTags = React.useCallback( 11 (tag: (typeof tags)[number]) => { 12 setUrl((curr) => ({ 13 ...curr, 14 tags: curr.tags.find((t) => t.id === tag.id) 15 ? curr.tags.filter((t) => t.id !== tag.id) 16 : curr.tags.concat(tag), 17 })); 18 }, 19 [setUrl], 20 ); 21 22 return ( 23 <div> 24 <Field text="Tags"> 25 <div className="flex flex-wrap gap-2"> 26 {tags.map((tag) => ( 27 <Tag 28 active={!!urlState.tags.find((t) => t.id === tag.id)} 29 text={tag.value.text} 30 onClick={() => onChangeTags(tag)} 31 key={tag.id} 32 /> 33 ))} 34 </div> 35 </Field> 36 37 <input value={urlState.name} 38 onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }} 39 // Can update state immediately but sync change to url as needed 40 onBlur={() => setUrl()} 41 /> 42 </div> 43 ); 44} 45 46const tags = [ 47 { 48 id: '1', 49 value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') }, 50 }, 51 { 52 id: '2', 53 value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') }, 54 }, 55 { 56 id: '3', 57 value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') }, 58 }, 59];
1'use client'; 2 3import React from 'react'; 4import { useUrlState } from 'state-in-url/next'; 5 6const form: Form = { 7 name: '', 8 age: undefined, 9 agree_to_terms: false, 10 tags: [], 11}; 12 13type Form = { 14 name: string; 15 age?: number; 16 agree_to_terms: boolean; 17 tags: {id: string; value: {text: string; time: Date } }[]; 18}; 19 20export const useFormState = ({ searchParams }: { searchParams?: object }) => { 21 const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, { 22 searchParams, 23 }); 24 25 // first navigation will push new history entry 26 // all following will just replace that entry 27 // this way will have history with only 2 entries - ['/url', '/url?key=param'] 28 29 const replace = React.useRef(false); 30 const setUrl = React.useCallback(( 31 state: Parameters<typeof setUrlBase>[0], 32 opts?: Parameters<typeof setUrlBase>[1] 33 ) => { 34 setUrlBase(state, { replace: replace.current, ...opts }); 35 replace.current = true; 36 }, [setUrlBase]); 37 38 return { urlState, setUrl, resetUrl: reset }; 39};
1export const form: Form = { 2 name: '', 3 age: undefined, 4 agree_to_terms: false, 5 tags: [], 6}; 7 8type Form = { 9 name: string; 10 age?: number; 11 agree_to_terms: boolean; 12 tags: { id: string; value: { text: string; time: Date } }[]; 13};
1'use client' 2import { useUrlState } from 'state-in-url/next'; 3 4import { form } from './form'; 5 6function TagsComponent() { 7 // `urlState` will infer from Form type! 8 const { urlState, setUrl } = useUrlState(form); 9 10 const onChangeTags = React.useCallback( 11 (tag: (typeof tags)[number]) => { 12 setUrl((curr) => ({ 13 ...curr, 14 tags: curr.tags.find((t) => t.id === tag.id) 15 ? curr.tags.filter((t) => t.id !== tag.id) 16 : curr.tags.concat(tag), 17 })); 18 }, 19 [setUrl], 20 ); 21 22 return ( 23 <div> 24 <Field text="Tags"> 25 <div className="flex flex-wrap gap-2"> 26 {tags.map((tag) => ( 27 <Tag 28 active={!!urlState.tags.find((t) => t.id === tag.id)} 29 text={tag.value.text} 30 onClick={() => onChangeTags(tag)} 31 key={tag.id} 32 /> 33 ))} 34 </div> 35 </Field> 36 </div> 37 ); 38} 39 40const tags = [ 41 { 42 id: '1', 43 value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') }, 44 }, 45 { 46 id: '2', 47 value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') }, 48 }, 49 { 50 id: '3', 51 value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') }, 52 }, 53];
1 2const timer = React.useRef(0 as unknown as NodeJS.Timeout); 3React.useEffect(() => { 4 clearTimeout(timer.current); 5 timer.current = setTimeout(() => { 6 // will compare state by content not by reference and fire update only for new values 7 setUrl(urlState); 8 }, 500); 9 10 return () => { 11 clearTimeout(timer.current); 12 }; 13}, [urlState, setUrl]);
Syncing state onBlur
will be more aligned with real world usage.
1<input onBlur={() => updateUrl()} .../>
useUrlStateBase
hook for others routersHooks to create your own useUrlState
hooks with other routers, e.g. react-router or tanstack router.
useSharedState
hook for React.jsHook to share state between any React components, tested with Next.js and Vite.
1'use client' 2import { useSharedState } from 'state-in-url'; 3 4export const someState = { name: '' }; 5 6function SettingsComponent() { 7 const { state, setState } = useSharedState(someState); 8}
useUrlEncode
hook for React.jsencodeState
and decodeState
helpersencode
and decode
helpersCan create state hooks for slices of state, and reuse them across application. For example:
1type UserState = { 2 name: string; 3 age: number; 4 other: { id: string, value: number }[] 5}; 6const userState = { 7 name: '', 8 age: 0, 9 other: [], 10}; 11 12export const useUserState = () => { 13 const { urlState, setUrl, reset } = useUrlState(userState); 14 15 // other logic 16 17 // reset query params when navigating to other page 18 React.useEffect(() => { 19 return reset 20 }, []) 21 22 return { userState: urlState, setUserState: setUrl };; 23} 24
Function
, BigInt
or Symbol
won't work, probably things like ArrayBuffer
neither. Everything that can be serialized to JSON will work.next.js
14/15 with app router, no plans to support pages.See Contributing doc
Next.js
react-router
remix
svelte
astro
This project is licensed under the MIT license.
No vulnerabilities found.
nuqs
Type-safe search params state manager for React - Like useState, but stored in the URL query string
use-query-params
React Hook for managing state in URL query parameters with easy serialization.
@braneframe/plugin-url-sync
DXOS Surface plugin for keeping the browser url in sync with the application state
react-use-url-state
React hook for managing state in the URL