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

v2.3.0

Published

Library to store state in browser URL, includes hook for NextJS, hook for pure React, and low level helpers. https://state-in-url-asmyshlyaev177.vercel.app/

Downloads

750

Readme

English | 한국어 | 简体中文

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

Demo-gif

DEMO | DEMO2 | Demo on Codesandbox

Add a ⭐️ and follow me to support the project!

Why use state-in-url?

state-in-url Simple state management with optional URL sync. Share complex states between unrelated React components, TS-friendly, NextJS compatible. Most of users don't care about URL, so, can use it to store your app state.

Use cases

  • 🧮 Store unsaved user forms in URL
  • 🙃 Share the state between different components without changing url, good as alternative to signals and other state management tools
  • 🧠 Sync data between unrelated client components
  • 🔗 Shareable URLs with full application state
  • 🔄 Easy state persistence across page reloads

Features

  • 🧩 Simple: No providers, reducers, boilerplate or new concepts, API similar to React.useState
  • 📘 Typescript support and type Safety: Preserves data types and structure, good 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, high quality and support
  • Fast: Minimal rerenders
  • 🪶 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

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

useUrlState hook for Next.js

Docs

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

  1. Define state shape

    // userState.ts
    // State shape should be stored in a constant, don't pass an object directly
    export const userState: UserState = { name: '', age: 0 }
    
    type UserState = { name: string, age: number }
  2. 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 `updateUrl` will use `rounter.push` or `router.replace`, default replace=true
  // const { state, updateState, updateUrl } = useUrlState({ defaultState: userState, searchParams, replace: false });
  const { state, updateState, updateUrl } = useUrlState({ defaultState: userState });

  // won't let you to accidently mutate state directly, requires TS
  // state.name = 'John' // <- error

  return (
    <div>
      <input value={state.name}
        onChange={(ev) => { updateState({ name: ev.target.value }) }}
        onBlur={() => updateUrl()}
      />
      <input value={state.age}
        onChange={(ev) => { updateState({ age: +ev.target.value }) }}
        onBlur={() => updateUrl()}
      />

      // same api as React.useState
      <input value={state.name}
        onChange={(ev) => { updateState(curr => ({ ...curr, name: ev.target.value })) }}
        onBlur={() => updateUrl()}
      />

      <button onClick={() => updateUrl(userState)}>
        Reset
      </button>

    </div>
  )
}

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() {
  // `state` will infer from Form type!
  const { state, updateUrl } = useUrlState({ defaultState: form });

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

  return (
    <div>
      <Field text="Tags">
        <div className="flex flex-wrap gap-2">
          {tags.map((tag) => (
            <Tag
              active={!!state.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

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

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 { state, updateState, updateUrl } = useUrlState({ defaultState: form, searchParams });
}

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

useUrlStateBase hook for others routers

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

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

Docs

useUrlEncode hook for React.js

Docs

encodeState and decodeState helpers

Docs

encode and decode helpers

Docs

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
  • Use Suspence to wrap client components in Next.js
  • Use this extension for readable TS errors

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
  3. 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

Roadmap

  • [x] hook for Next.js
  • [ ] hook for 'react-router`
  • [ ] 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

Using URL to store state in Vue

Storing state in the URL

NextJS useSearchParams