JSPM

  • Created
  • Published
  • Downloads 20800
  • Score
    100M100P100Q159917F
  • License ISC

Library to store state in browser URL, includes hook for NextJS, hook for pure React, and low level helpers.

Package Exports

  • state-in-url

Readme

state-in-url

Library logo
Seamlessly store complex nested state object in URL query parameters with IDE autotomplete and TS validation.

For hire

npm Tests Commitizen friendly npm bundle size (minified + gzip)

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

DEMO


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

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]);
'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 and updateUrl to sync changes to url

Gothas

  1. Can pass only serializable values, Function, BigInt or Symbol won't work, probably things like ArrayBuffer neither.
  2. 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

Changelog

License

This project is licensed under the MIT license.

Inspiration

Using URL to store state in Vue

Storing state in the URL

NextJS useSearchParams