Package Exports
- state-in-url
Readme
state-in-url
Don't hesitate to open issue if you found a bug
https://github.com/asmyshlyaev177/state-in-url/assets/19854148/10456887-f149-4745-b3f1-e799e70b16cd
Add a âïļ to support the project!
Why use state-in-url?
state-in-url
lets you use URI string for state management.
Use cases
- ð Shareable URLs with full application state
- ð Easy state persistence across page reloads
- ð§ Pass data between unrelated client components
- ð Can just share the state without changing url
- ð§Ū Store unsaved user forms in URL
Features
- ð§Đ Simple: Handles complex objects without extra effort
- ð 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
- ðŠķ 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 store and retrieve state from the URL search parameters, providing a way to persist state across page reloads and share application state via URLs.
Usage examples
Basic
'use client'
import { useUrlState } from 'state-in-url';
// State shape should be stored in a constant, don't pass an object directly
const countState: { count: number } = { count: 0 };
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
'use client'
import { useUrlState } from 'state-in-url';
interface UserSettings {
theme: 'light' | 'dark';
fontSize: number;
notifications: boolean;
}
const defaultSettings: UserSettings {
theme: 'light',
fontSize: 16,
notifications: true,
}
function SettingsComponent() {
// `state` will infer from UserSettings type!
const { state, updateUrl } = useUrlState(defaultSettings);
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
useUrlEncode
is a custom React hook that provides utility functions for encoding and decoding state object to and from URL search parameters. This hook doesn't depend on Nextjs, and will works with any React application.
Accepts optional defaultState
argument.
import { useUrlEncode } from 'state-in-url';
const Component = () => {
const { parse, stringify } = useUrlEncode();
const str = stringify({ age: 36 }); // age=â36
const obj = parse(str); // { age: 36 }
const currentParams = parse(window.location.search);
// OR
// const obj = parse(new URLSearchParams(window.location.search))
const updateSearch = () => {
const currentParams = new URLSearchParams(window.location.search);
const newState = { query: 'react hooks', page: 2 };
const updatedParamsString = stringify(newState, currentParams);
console.log(updatedParamsString);
// Output: existing params + query=react%20hooks&page=2
};
}
encodeState
and decodeState
helpers
encodeState
let you encode some object with optional defaults
, and optional existing queryString
export const form = { name: '' };
...
encodeState({ name: 'test' }, form, 'someExistingParam=123');
decodeState
let you decode queryString with optional defaults
export const form = { name: '' };
...
decodeState('name=Alex', form);
encode
and decode
helpers
There low level helpers to stringify and parse query string params. Useful for other frameworks or pure JS.
import { encode, decode } from 'state-in-url';
const state = { obj: [1, 2, 3], obj2: true }
// to params
const params = new URLSearchParams();
Object.entries(state).forEach(([key, value]) => {
params.set(key, encode(value));
});
const str = params.toString();
// from params
const obj = Object.fromEntries(
[...params.entries()].map(([key, value]) => [
key,
// decode(value, optionalFallback),
decode(value),
]),
)
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
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
- Add a âïļ star on GitHub to support the project!
Changelog
License
This project is licensed under the MIT license.