Estado tipado, vivendo na URL

useUrlState é o estado React que se grava na string de consulta. Objetos, arrays e datas mantêm seus tipos, cada estado é um link compartilhável e sobrevive a recarregamentos — sem providers, sem boilerplate.

  • ~2 KB em gzip
  • zero dependências
  • TypeScript-first
  • Next.js / react-router / Remix
  • MIT
npm i state-in-url

useUrlState — ao vivo com next.js

Digite abaixo — veja a URL acender

Primeiro componente de cliente
Outro componente de cliente

Lê da URL — sem props, sem context, os tipos e a estrutura são preservados

{
name: string
age: undefinedundefined
agree_to_terms: falseboolean
tags: []
}

A mesma API, três routers

Início rápido

1. Defina o estado
state
export const form: Form = {
  name: '',
  age: undefined,
  agree_to_terms: false,
  tags: [],
};

// use `Type` not `Interface`!
type Form = {
  name: string;
  age?: number;
  agree_to_terms: boolean;
  tags: { id: string; value: { text: string; time: Date } }[];
};
2. Use em qualquer componente
ComponentA
'use client';

import { useUrlState } from 'state-in-url/next';
import { form } from './form';

export const ComponentA = () => {
  // see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
  // useHistory: false to update sp on server component
  const { urlState, setState, setUrl } = useUrlState(form, { useHistory: true }); 

  return <>
    <input
      id="name"
      value={urlState.name} 
      onChange={(ev) => setUrl({ name: ev.target.value })}
      />
    // OR can update state immediately but sync change to url as needed
    <input
      value={urlState.name}
      onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
      onBlur={() => setUrl()}
    />
    <button onClick={() => setUrl((curr, initial) => initial)}>
      Reset
    </button>
    </>
};
ComponentB
'use client';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';

// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
  const { urlState } = useUrlState(form, { searchParams });

// will be defaultValue from `form` if not in url, no need to check

  return <div>name: {urlState.name}</div>
};
3. Crie um hook reutilizável para uma parte do estado
useFormState - custom hook
'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 };
};

Usando um agente de codificação de IA?

Os agentes erram as mesmas duas coisas aqui, todas as vezes. Eles definem a forma do estado com interface, que a restrição JSONCompatible rejeita de imediato. E eles constroem o objeto de estado padrão dentro do componente, o que quebra o compartilhamento em silêncio — ele é indexado pela identidade do objeto, então nada gera erro, os dois componentes simplesmente param de se ver.

Então o pacote inclui seis SKILL.md arquivos. Seu agente carrega um sob demanda por meio do TanStack Intent, e eles são versionados com a biblioteca e não com esta página.

npx @tanstack/intent@latest install

Execute uma vez em um projeto que já tenha state-in-url instalado. Seu agente então encontra as habilidades em node_modules/state-in-url/skills/.

  • feature-state-hookDefinir o estado e envolver o useUrlState em um hook com escopo de funcionalidade
  • input-handlingCampos de texto, sliders, qualquer coisa que muda rápido
  • nextjs-ssrApp Router: encaminhamento de searchParams, Proxy para layouts
  • react-router-remix-setupConfiguração do React Router v6/v7 ou Remix v2
  • form-library-integrationCombinando com react-hook-form (ou formik)
  • shared-state-no-urluseSharedState — compartilhar sem tocar na URL

As fontes estão no GitHub. Um agente que não consegue carregar as habilidades do Intent deve ler llms.txt em vez disso — as mesmas regras, condensadas em um arquivo.

Por que state-in-url?

Existem bibliotecas de estado na URL, mas a maioria é complicada de configurar ou limitada no que pode armazenar. state-in-url pretende ser a que simplesmente funciona: uma API que espelha React.useState, com a URL como armazenamento.

Armazene estado sem boilerplate, construa links profundos e compartilhe dados entre componentes de cliente não relacionados — sem necessidade de provider. A estrutura e os tipos são preservados de ponta a ponta: um Date entra, um Date sai.

Construído com test-first, com suítes unitárias e e2e entre navegadores executando a cada commit.

Next.js: sem limite de Suspense

O hook nunca chama useSearchParams, então um componente que o usa não precisa ser envolvido em Suspense e não exclui sua página da pré-renderização — PPR e cacheComponents incluídos. Ele lê a URL diretamente e acompanha cada alteração posterior, incluindo um history.pushState de um código que nada sabe sobre ele.

Não usa Next.js ou react-router?

Os helpers encodeState / decodeState funcionam com qualquer framework ou JS puro — os hooks são uma conveniência por cima.

Confira a página no GitHub — uma estrela ajuda muito.

Compartilhe com outros desenvolvedores

Uneed Embed Badge