Package Exports
- state-in-url
- state-in-url/encodeState
- state-in-url/encoder
- state-in-url/next
- state-in-url/useSharedState
- state-in-url/useUrlEncode
- state-in-url/useUrlStateBase
- state-in-url/utils
Readme
state-in-url
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 sync. Share complex states between unrelated React components, sync state to URL, TS-friendly, NextJS compatible.
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.jsuseSharedState
for React.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
In tsconfig.json
in compilerOptions
set "moduleResolution": "Node16"
or "moduleResolution": "NodeNext"
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/next';
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/next';
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 server side rendering
export default async function Home({ searchParams }: { searchParams: object }) {
return (
<Form sp={searchParams} />
)
}
// Form.tsx
'use client'
import React from 'react';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
const Form = ({ sp }: { sp: object }) => {
const { state, updateState, updateUrl } = useUrlState(form, sp);
}
Using hook in layout
component
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`
export const runtime = 'edge';
// middleware.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const url = request.url?.includes('_next') ? null : request.url;
const sp = url?.split?.('?')?.[1] || '';
const response = NextResponse.next();
if (url !== null) {
response.headers.set('searchParams', sp);
}
return response;
}
// Target layout component
import { headers } from 'next/headers';
import { decodeState } from 'state-in-url/encodeState';
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const sp = headers().get('searchParams') || '';
return (
<div>
<Comp1 searchParams={decodeState(sp, stateShape)} />
{children}
</div>
);
}
With arbitrary state shape (not recommended)
'use client'
import { useUrlState } from 'state-in-url/next';
const someObj = {};
function SettingsComponent() {
const { state, updateUrl, updateState } = useUrlState<object>(someObj);
}
useSharedState
hook for React.js
Hook to share state between any React components, tested with Next.js and Vite.
'use client'
import { useSharedState } from 'state-in-url';
export const someState = { name: '' };
function SettingsComponent() {
const { state, setState } = useSharedState(someState);
}
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
Suspence
to wrap client components in Next.js - 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
- Tested with
next.js
14 with app router, no plans to support pages.
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.