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, and react-router@6-7.
npm install state-in-url
Module System
Min. Node Version
Typescript Support
Node Version
NPM Version
157 Stars
571 Commits
3 Forks
3 Watching
2 Branches
3 Contributors
Updated on 28 Nov 2024
Minified
Minified + Gzipped
TypeScript (94.62%)
JavaScript (2.83%)
CSS (2.26%)
HTML (0.3%)
Cumulative downloads
Total Downloads
Last day
124.6%
53,043
Compared to previous day
Last week
30.5%
275,722
Compared to previous week
Last month
3,960.9%
916,090
Compared to previous month
Last year
0%
943,435
Compared to previous year
58
English | í•œêµì–´ | 简体ä¸æ–‡
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 JS1# 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 9 const { urlState, setUrl, setState, reset } = 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={reset}> 29 Reset 30 </button> 31 32 </div> 33 ) 34}
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()} .../>
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/react-router'; 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];
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.
Reason
no binaries found in the repo
Reason
no dangerous workflow patterns detected
Reason
update tool detected
Details
Reason
license file detected
Details
Reason
30 commit(s) out of 30 and 7 issue activity out of 8 found in the last 90 days -- score normalized to 10
Reason
publishing workflow detected
Details
Reason
all dependencies are pinned
Details
Reason
GitHub workflow tokens follow principle of least privilege
Details
Reason
1 existing vulnerabilities detected
Details
Reason
badge detected: passing
Reason
security policy file detected
Details
Reason
1 different organizations found -- score normalized to 3
Details
Reason
found 30 unreviewed changesets out of 30 -- score normalized to 0
Reason
project is not fuzzed
Details
Reason
no SAST tool detected
Details
Score
Last Scanned on 2024-11-28T19:53:57Z
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More