Package Exports
- state-in-url
Readme
state-in-url
Easily share state between unrelated React components, with IDE autocomplete and TS validation. Without any hasssle or boilerplate.
Don't hesitate to open an issue if you found a bug
Add a ⭐️ to support the project!
Why use state-in-url?
state-in-url
Simple state management with URL synchronization.
Use cases
- 🙃 Share the state between different components without changing url, good as alternative to signals and other state management tools
- 🔗 Shareable URLs with full application state
- 🔄 Easy state persistence across page reloads
- 🧠 Sync data between unrelated client components
- 🧮 Store unsaved user forms in URL
Features
- 🧩 Simple: No providers, reducers, boilerplate or new concepts
- 📘 Typescript support and type Safety: Preserves data types and structure, enhances developer experience with IDE suggestions, strong typing and JSDoc comments
- ⚛️ Framework Flexibility: Separate hooks for Next.js and React.js applications, and functions for pure JS
- ⚙ Well tested: Unit tests and Playwright tests
- ⚡ Fast: Minimal rerenders
- 🪶 Lightweight: Zero dependencies for a smaller footprint
Table of content
- Installation
useUrlState
for Next.jsuseUrlEncode
for React.jsencodeState
anddecodeState
for pure JS usage- auto sync state with url
- Low-level
encode
anddecode
functions - Best practices
- Gothas
- Contact & Support
- Changelog
- License
- Inspiration
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
set "moduleResolution": "Node16"
or "moduleResolution": "NodeNext"
in your tsconfig.json
useUrlState hook for Next.js
useUrlState
is a custom React hook for Next.js applications that make communication between client components easy. It allows you to share any complex state and sync it with the URL search parameters, providing a way to persist state across page reloads and share application state via URLs.
Usage examples
Basic
Define state shape
// countState.ts // State shape should be stored in a constant, don't pass an object directly export const countState: CountState = { count: 0 } type CountState = { count: number }
Import it and use
'use client'
import { useUrlState } from 'state-in-url';
import { countState } from './countState';
function MyComponent() {
// for use searchParams from server component
// e.g. export default async function Home({ searchParams }: { searchParams: object }) {
// const { state, updateState, updateUrl } = useUrlState(countState, searchParams);
const { state, updateState, updateUrl } = useUrlState(countState);
// won't let you to accidently mutate state directly, requires TS
// state.count = 2 // <- error
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => updateUrl({ count: state.count + 1 }), { replace: true }}>
Increment (Update URL)
</button>
// same api as React.useState
<button onClick={() => updateState(currState => ({...currState, count: currState.count + 1 }) )}>
Increment (Local Only)
</button>
<button onClick={() => updateUrl()}>
Sync changes to url
// Or don't sync it and just share state
</button>
<button onClick={() => updateUrl(state)}>
Reset
</button>
</div>
)
}
With complex state shape
interface UserSettings {
theme: 'light' | 'dark';
fontSize: number;
notifications: boolean;
}
export const userSettings: UserSettings {
theme: 'light',
fontSize: 16,
notifications: true,
}
'use client'
import { useUrlState } from 'state-in-url';
import { userSettings } from './userSettings';
function SettingsComponent() {
// `state` will infer from UserSettings type!
const { state, updateUrl } = useUrlState(userSettings);
const toggleTheme = () => {
updateUrl(current => ({
...current,
theme: current.theme === 'light' ? 'dark' : 'light',
}));
};
// sync state to url when idle
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
updateUrl(state);
}, 500);
return () => {
clearTimeout(timer.current);
};
}, [state, updateUrl]);
return (
<div>
<h2>User Settings</h2>
<p>Theme: {state.theme}</p>
<p>Font Size: {state.fontSize}px</p>
<button onClick={toggleTheme}>Toggle Theme</button>
{/* Other UI elements to update other settings */}
</div>
);
}
...
// Other component
function Component() {
const { state } = useUrlState(defaultSettings);
return (
<div>
<p>Notifications is {state.notifications ? 'On' : 'Off'}</p>
</div>
)
}
Auto sync state
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
updateUrl(state);
}, 500);
return () => {
clearTimeout(timer.current);
};
}, [state, updateUrl]);
With arbitrary state shape (not recommended)
'use client'
import { useUrlState } from 'state-in-url';
const someObj = {};
function SettingsComponent() {
const { state, updateUrl, updateState } = useUrlState<object>(someObj);
}
useUrlEncode
hook for React.js
encodeState
and decodeState
helpers
encode
and decode
helpers
Best Practices
- Define your state shape as a constant to ensure consistency
- Use TypeScript for enhanced type safety and autocomplete
- Avoid storing sensitive information in URL parameters
- Use
updateState
for frequent updates andupdateUrl
to sync changes to url - Use this extension for readable TS errors
Gothas
- Can pass only serializable values,
Function
,BigInt
orSymbol
won't work, probably things likeArrayBuffer
neither. - 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
Run locally
Clone this repo, run npm install
and
npm run dev
Go to localhost:3000
Contact & Support
- Create a GitHub issue for bug reports, feature requests, or questions
Changelog
License
This project is licensed under the MIT license.