- Notifications
You must be signed in to change notification settings - Fork5
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, and react-router@6-7.
License
asmyshlyaev177/state-in-url
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
URI size limitation,up to 12KB is safe
Add a andfollow me to support the project!
Will appreciate you feedback/opinion ondiscussions
Share if it useful for you.X.comLinkedInFBVK
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.
ContainsuseUrlState
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
- Sycn 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, around1ms to encode and decode big object
- Server Side Rendering: Can use it in Server Components, Next.js 14 and 15 are supported
- 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 - Well tested:Unit tests and Playwright tests for Chrome/Firefox/Safari
- Permissive license: MIT
# npmnpm install --save state-in-url# yarnyarn add state-in-url# pnpmpnpm add state-in-url
Intsconfig.json
incompilerOptions
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 samestate
object are automatically synchronized.
- Define state shape with default values
// userState.ts// Only parameters with value different from default will go to the url.exportconstuserState:UserState={name:'',age:0}// use `Type` not `Interface`!typeUserState={name:string,age:number}
- Import it and use
'use client'import{useUrlState}from'state-in-url/next';import{userState}from'./userState';functionMyComponent(){// 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, pass `useHistory: false` if you need to fetch smt in the server componentconst{ urlState, setUrl, setState, reset}=useUrlState(userState);return(<div>// urlState.name will return default value from `userState` if url empty<inputvalue={urlState.name}// same api as React.useState, e.g. setUrl(currVal => currVal + 1)onChange={(ev)=>setUrl({name:ev.target.value})}/><inputvalue={urlState.age}onChange={(ev)=>setUrl({age:+ev.target.value})}/><inputvalue={urlState.name}onChange={(ev)=>{setState(curr=>({ ...curr,name:ev.target.value}))}}// Can update state immediately but sync change to url as neededonBlur={()=>setUrl()}/><buttononClick={reset}>Reset</button></div>)}
Example
'use client';importReactfrom'react';import{useUrlState}from'state-in-url/next';constform:Form={name:'',age:undefined,agree_to_terms:false,tags:[],};typeForm={name:string;age?:number;agree_to_terms:boolean;tags:{id:string;value:{text:string;time:Date}}[];};exportconstuseFormState=({ 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']constreplace=React.useRef(false);constsetUrl=React.useCallback((state:Parameters<typeofsetUrlBase>[0],opts?:Parameters<typeofsetUrlBase>[1])=>{setUrlBase(state,{replace:replace.current, ...opts});replace.current=true;},[setUrlBase]);return{ urlState, setUrl,resetUrl:reset};};
Example
exportconstform:Form={name:'',age:undefined,agree_to_terms:false,tags:[],};typeForm={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';functionTagsComponent(){// `urlState` will infer from Form type!const{ urlState, setUrl}=useUrlState(form);constonChangeTags=React.useCallback((tag:(typeoftags)[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><Fieldtext="Tags"><divclassName="flex flex-wrap gap-2">{tags.map((tag)=>(<Tagactive={!!urlState.tags.find((t)=>t.id===tag.id)}text={tag.value.text}onClick={()=>onChangeTags(tag)}key={tag.id}/>))}</div></Field></div>);}consttags=[{id:'1',value:{text:'React.js',time:newDate('2024-07-17T04:53:17.000Z')},},{id:'2',value:{text:'Next.js',time:newDate('2024-07-18T04:53:17.000Z')},},{id:'3',value:{text:'TailwindCSS',time:newDate('2024-07-19T04:53:17.000Z')},},];
Example
consttimer=React.useRef(0asunknownasNodeJS.Timeout);React.useEffect(()=>{clearTimeout(timer.current);timer.current=setTimeout(()=>{// will compare state by content not by reference and fire update only for new valuessetUrl(urlState);},500);return()=>{clearTimeout(timer.current);};},[urlState,setUrl]);
Syncing stateonBlur
will be more aligned with real world usage.
<inputonBlur={()=>updateUrl()} .../>
Example
exportdefaultasyncfunctionHome({ searchParams}:{searchParams:object}){return(<FormsearchParams={searchParams}/>)}// Form.tsx'use client'importReactfrom'react';import{useUrlState}from'state-in-url/next';import{form}from'./form';constForm=({ searchParams}:{searchParams:object})=>{const{ urlState, setState, setUrl}=useUrlState(form,{ searchParams});}
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`exportconstruntime='edge';// middleware.tsimporttype{NextRequest}from'next/server';import{NextResponse}from'next/server';exportfunctionmiddleware(request:NextRequest){consturl=request.url?.includes('_next') ?null :request.url;constsp=url?.split?.('?')?.[1]||'';constresponse=NextResponse.next();if(url!==null){response.headers.set('searchParams',sp);}returnresponse;}// Target layout componentimport{headers}from'next/headers';import{decodeState}from'state-in-url/encodeState';exportdefaultasyncfunctionLayout({ children,}:{children:React.ReactNode;}){constsp=headers().get('searchParams')||'';return(<div><Comp1searchParams={decodeState(sp,stateShape)}/>{children}</div>);}
Example
'use client'import{useUrlState}from'state-in-url/next';constsomeObj={};functionSettingsComponent(){const{ urlState, setUrl, setState}=useUrlState<object>(someObj);}
API is same as for Next.js version, except can pass options fromNavigateOptions type.
exportconstform:Form={name:'',age:undefined,agree_to_terms:false,tags:[],};typeForm={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';functionTagsComponent(){const{ urlState, setUrl, setState}=useUrlState(form);constonChangeTags=React.useCallback((tag:(typeoftags)[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><Fieldtext="Tags"><divclassName="flex flex-wrap gap-2">{tags.map((tag)=>(<Tagactive={!!urlState.tags.find((t)=>t.id===tag.id)}text={tag.value.text}onClick={()=>onChangeTags(tag)}key={tag.id}/>))}</div></Field><inputvalue={urlState.name}onChange={(ev)=>{setState(curr=>({ ...curr,name:ev.target.value}))}}// Can update state immediately but sync change to url as neededonBlur={()=>setUrl()}/></div>);}consttags=[{id:'1',value:{text:'React.js',time:newDate('2024-07-17T04:53:17.000Z')},},{id:'2',value:{text:'Next.js',time:newDate('2024-07-18T04:53:17.000Z')},},{id:'3',value:{text:'TailwindCSS',time:newDate('2024-07-19T04:53:17.000Z')},},];
Hooks to create your ownuseUrlState
hooks with other routers, e.g. react-router or tanstack router.
Hook to share state between any React components, tested with Next.js and Vite.
'use client'import{useSharedState}from'state-in-url';exportconstsomeState={name:''};functionSettingsComponent(){const{ state, setState}=useSharedState(someState);}
- 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 thisextension for readable TS errors
Can create state hooks for slices of state, and reuse them across application. For example:
typeUserState={name:string;age:number;other:{id:string,value:number}[]};constuserState={name:'',age:0,other:[],};exportconstuseUserState=()=>{const{ urlState, setUrl, reset}=useUrlState(userState);// other logic// reset query params when navigating to other pageReact.useEffect(()=>{returnreset},[])return{userState:urlState,setUserState:setUrl};;}
- 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) to14KB, 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.
- hook for
Next.js
- hook for
react-router
- hook for
remix
- hook for
svelte
- hook for
astro
- hook for store state in hash ?
- Create aGitHub issue for bug reports, feature requests, or questions
This project is licensed under theMIT license.
About
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, and react-router@6-7.