npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

state-in-url

v4.1.3

Published

Easily share complex state objects between unrelated React components, preserve types and structure, with TS validation. Deep links and url state synchronization wthout any hasssle or boilerplate.

Downloads

661,454

Readme

English | 한국어 | 简体中文

State in url

npm npm bundle size (minified + gzip) Codacy Badge

Tests Codacy Badge Commitizen friendly semantic-release: angular

OpenSSF Scorecard OpenSSF Best Practices

Demo-gif

DEMO | DEMO2

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

Why use 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.

Use cases

  • Store unsaved user forms or page filters in URL
  • Sycn URL with React state
  • Just sync data between unrelated client components without touching URI
  • Shareable URLs with application state (Deep linking, URL state synchronization)
  • Easy state persistence across page reloads

Features

  • 🧩 Simple: No providers, reducers, boilerplate or new concepts, API similar to React.useState
  • 📘 Typescript validation/autocomplete: State is just an object, automatic static validation in IDE/tests according to Typescript definition
  • Complex data: Nested objects, dates and arrays, works same as JSON, but in URL
  • Default values: Giving you default values if parameter not in url
  • Organized: All possible values defined at start, protect you from getting non existing key
  • Fast: Minimal rerenders, around 1ms to encode and decode big object
  • Server Side Rendering: Can use it in Server Components, Next.js 14 and 15 are supported
  • Lightweight: Zero dependencies, library less than 2KB
  • DX: Good developer experience, documentation, JSDoc comments, and examples
  • Framework Flexibility: Hooks for Next.js and react-router, helpers to use it with other frameworks or pure JS
  • Well tested: Unit tests and Playwright tests for Chrome/Firefox/Safari
  • Permissive license: MIT

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

In tsconfig.json in compilerOptions set "moduleResolution": "Bundler", or"moduleResolution": "Node16", or "moduleResolution": "NodeNext". Possibly need to set "module": "ES2022", or "module": "ESNext"

useUrlState

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.

useUrlState hook for Next.js

Full API Docs

React-Router example

Usage examples

Basic
  1. Define state shape with default values
// userState.ts
// Only parameters with value different from default will go to the url.
export const userState: UserState = { name: '', age: 0 }

// use `Type` not `Interface`!
type UserState = { name: string, age: number }
  1. Import it and use
'use client'
import { useUrlState } from 'state-in-url/next';

import { userState } from './userState';

function MyComponent() {
  // can pass `replace` arg, it's control will `setUrl` will use `rounter.push` or `router.replace`, default replace=true
  // can pass `searchParams` from server components
  const { urlState, setUrl, setState, reset } = useUrlState(userState);

  return (
    <div>
      // urlState.name will return default value from `userState` if url empty
      <input value={urlState.name}
        // same api as React.useState, e.g. setUrl(currVal => currVal + 1)
        onChange={(ev) => setUrl({ name: ev.target.value }) }
      />
      <input value={urlState.age}
        onChange={(ev) => setUrl({ age: +ev.target.value }) }
      />

      <input value={urlState.name}
        onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
        // Can update state immediately but sync change to url as needed
        onBlur={() => setUrl()}
      />

      <button onClick={reset}>
        Reset
      </button>

    </div>
  )
}
Custom hook to work with slice of state conveniently
'use client';

import React from 'react';
import { useUrlState } from 'state-in-url/next';

const form: Form = {
  name: '',
  age: undefined,
  agree_to_terms: false,
  tags: [],
};

type Form = {
  name: string;
  age?: number;
  agree_to_terms: boolean;
  tags: {id: string; value: {text: string; time: Date } }[];
};

export const useFormState = ({ searchParams }: { searchParams?: object }) => {
  const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
    searchParams,
  });

  // first navigation will push new history entry
  // all following will just replace that entry
  // this way will have history with only 2 entries - ['/url', '/url?key=param']

  const replace = React.useRef(false);
  const setUrl = React.useCallback((
      state: Parameters<typeof setUrlBase>[0],
      opts?: Parameters<typeof setUrlBase>[1]
    ) => {
      setUrlBase(state, { replace: replace.current, ...opts });
      replace.current = true;
  }, [setUrlBase]);

  return { urlState, setUrl, resetUrl: reset };
};
With complex state shape
export const form: Form = {
  name: '',
  age: undefined,
  agree_to_terms: false,
  tags: [],
};

type Form = {
  name: string;
  age?: number;
  agree_to_terms: boolean;
  tags: { id: string; value: { text: string; time: Date } }[];
};
'use client'
import { useUrlState } from 'state-in-url/next';

import { form } from './form';

function TagsComponent() {
  // `urlState` will infer from Form type!
  const { urlState, setUrl } = useUrlState(form);

  const onChangeTags = React.useCallback(
    (tag: (typeof tags)[number]) => {
      setUrl((curr) => ({
        ...curr,
        tags: curr.tags.find((t) => t.id === tag.id)
          ? curr.tags.filter((t) => t.id !== tag.id)
          : curr.tags.concat(tag),
      }));
    },
    [setUrl],
  );

  return (
    <div>
      <Field text="Tags">
        <div className="flex flex-wrap gap-2">
          {tags.map((tag) => (
            <Tag
              active={!!urlState.tags.find((t) => t.id === tag.id)}
              text={tag.value.text}
              onClick={() => onChangeTags(tag)}
              key={tag.id}
            />
          ))}
        </div>
      </Field>
    </div>
  );
}

const tags = [
  {
    id: '1',
    value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
  },
  {
    id: '2',
    value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
  },
  {
    id: '3',
    value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
  },
];

Demo page example code

Update state only and sync to URL manually

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
    setUrl(urlState);
  }, 500);

  return () => {
    clearTimeout(timer.current);
  };
}, [urlState, setUrl]);

Syncing state onBlur will be more aligned with real world usage.

<input onBlur={() => updateUrl()} .../>
With server side rendering
export default async function Home({ searchParams }: { searchParams: object }) {
  return (
    <Form searchParams={searchParams} />
  )
}

// Form.tsx
'use client'
import React from 'react';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';

const Form = ({ searchParams }: { searchParams: object }) => {
  const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
}
Using hook in layout component
// 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 { urlState, setUrl, setState } = useUrlState<object>(someObj);
}

useUrlState hook for React-Router

API is same as for Next.js version, except can pass options from NavigateOptions type.

API Docs

Example

export const form: Form = {
  name: '',
  age: undefined,
  agree_to_terms: false,
  tags: [],
};

type Form = {
  name: string;
  age?: number;
  agree_to_terms: boolean;
  tags: { id: string; value: { text: string; time: Date } }[];
};
import { useUrlState } from 'state-in-url/react-router';

import { form } from './form';

function TagsComponent() {
  const { urlState, setUrl, setState } = useUrlState(form);

  const onChangeTags = React.useCallback(
    (tag: (typeof tags)[number]) => {
      setUrl((curr) => ({
        ...curr,
        tags: curr.tags.find((t) => t.id === tag.id)
          ? curr.tags.filter((t) => t.id !== tag.id)
          : curr.tags.concat(tag),
      }));
    },
    [setUrl],
  );

  return (
    <div>
      <Field text="Tags">
        <div className="flex flex-wrap gap-2">
          {tags.map((tag) => (
            <Tag
              active={!!urlState.tags.find((t) => t.id === tag.id)}
              text={tag.value.text}
              onClick={() => onChangeTags(tag)}
              key={tag.id}
            />
          ))}
        </div>
      </Field>

      <input value={urlState.name}
        onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
        // Can update state immediately but sync change to url as needed
        onBlur={() => setUrl()}
      />
    </div>
  );
}

const tags = [
  {
    id: '1',
    value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
  },
  {
    id: '2',
    value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
  },
  {
    id: '3',
    value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
  },
];

Example code

Other hooks and helpers

useUrlStateBase hook for others routers

Hooks to create your own useUrlState hooks with other routers, e.g. react-router or tanstack router.

API Docs

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);
}

API Docs

useUrlEncode hook for React.js

API Docs

encodeState and decodeState helpers

API Docs

encode and decode helpers

API Docs

Best Practices

  • Define your state shape as a constant
  • Use TypeScript for enhanced type safety and autocomplete
  • Avoid storing sensitive information in URL parameters (SSN, API keys etc)
  • Use this extension for readable TS errors

Can create state hooks for slices of state, and reuse them across application. For example:

type UserState = {
  name: string;
  age: number;
  other: { id: string, value: number }[]
};
const userState = {
  name: '',
  age: 0,
  other: [],
};

export const useUserState = () => {
  const { urlState, setUrl } = useUrlState(userState);

  return { userState: urlState, setUserState: setUrl };;
}

Gotchas

  1. Can pass only serializable values, Function, BigInt or Symbol won't work, probably things like ArrayBuffer neither. Everything that can be serialized to JSON will work.
  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
  3. Tested with next.js 14/15 with app router, no plans to support pages.

Other

Contribute and/or run locally

See Contributing doc

Roadmap

  • [x] hook for Next.js
  • [x] hook for react-router
  • [ ] hook for remix
  • [ ] hook for svelte
  • [ ] hook for astro
  • [ ] hook for store state in hash ?

Contact & Support

  • Create a GitHub issue for bug reports, feature requests, or questions

Changelog

License

This project is licensed under the MIT license.

Inspiration

NUQS

Using URL to store state in Vue

Storing state in the URL

NextJS useSearchParams