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

state-in-url vs nuqs

Procurando uma alternativa ao nuqs? Ambas guardam estado tipado na query string; diferem em quanta configuração exigem e no que um valor pode ser.

O quêstate-in-urlnuqs
ConfiguraçãoNenhuma — importe o hook e prontoUm componente adaptador envolve o app
Formato do estadoUm objeto tipado, como React.useStateValores por chave, com um parser declarado para cada uma
Reuso entre componentesEnvolva o hook uma vez — cada componente compartilha o estado, sem propsVocê extrai seu próprio hook em volta do mapa de parsers
Objetos e arrays aninhadosNativo — estrutura e tipos preservadosParser JSON mais seu próprio validador
DatasPreservadas automaticamenteParser embutido, declarado por chave
Tamanho, import completo~2,9 KB gzip~6,7 KB gzip
Dependências em runtimeNenhumaUma
RoteadoresNext.js, React Router v6/v7, Remix, helpers para JS puroNext.js, React Router, Remix, TanStack Router, React puro

Tamanhos: import da biblioteca inteira, esbuild minify + gzip, medido em agosto de 2026 contra o nuqs 2.10.1.

nuqs é uma boa biblioteca — prefira-o se quiser cada valor como seu próprio query param legível, ou se usa TanStack Router. Prefira state-in-url quando quiser um objeto tipado inteiro na URL sem configuração.

A mesma feature, nas duas

Um painel de filtros: um texto de busca, um número de página, uma lista de tags e uma data. nuqs declara um parser por chave e liga um adaptador na raiz; state-in-url recebe o objeto e o envolve num hook reutilizável.

app/layout.tsx (nuqs)
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <NuqsAdapter>{children}</NuqsAdapter>
      </body>
    </html>
  );
}
filters.tsx (nuqs)
'use client';
import {
  useQueryStates,
  parseAsString,
  parseAsInteger,
  parseAsArrayOf,
  parseAsIsoDateTime,
} from 'nuqs';

export const Filters = () => {
  const [filters, setFilters] = useQueryStates({
    q: parseAsString.withDefault(''),
    page: parseAsInteger.withDefault(1),
    tags: parseAsArrayOf(parseAsString).withDefault([]),
    since: parseAsIsoDateTime,
  });

  return (
    <input
      value={filters.q}
      onChange={(ev) => setFilters({ q: ev.target.value, page: 1 })}
    />
  );
};
filters.tsx (state-in-url)
'use client';
import { useUrlState } from 'state-in-url/next';

export const filters = {
  q: '',
  page: 1,
  tags: [] as string[],
  since: undefined as Date | undefined,
};

// One reusable hook = the whole API for this feature
export const useFilters = () => useUrlState(filters);

export const SearchBox = () => {
  const { urlState, setUrl } = useFilters();

  return (
    <input
      value={urlState.q}
      onChange={(ev) => setUrl({ q: ev.target.value, page: 1 })}
    />
  );
};

export const ActiveTags = () => {
  // Same state, another component - no props, no context
  const { urlState } = useFilters();

  return <>{urlState.tags.join(', ')}</>; // tags is still string[]
};

Esse único hook customizado é toda a API da feature: cada componente que o chama compartilha o mesmo estado tipado — a lista de tags continua um array e a data volta como um objeto Date de verdade. Sem props, sem context, sem fiação por chave.

Configuração e boilerplate

nuqs se conecta ao seu roteador por um componente adaptador que envolve o app, e cada pedaço de estado declara seu parser. state-in-url traz um hook por roteador — importe o correspondente, passe um objeto de estado padrão e pronto. Nada envolve nada.

Next.js, SSR e pré-renderização

No App Router, state-in-url nunca chama useSearchParams, então componentes que o usam não precisam de um limite de Suspense e suas páginas continuam pré-renderizando — PPR incluído. Componentes de servidor leem o mesmo estado pela prop searchParams, repassada como está.

Migrando do nuqs

Quase sempre é mecânico: junte as chaves de uma feature num único objeto de estado padrão, remova as declarações de parser — valores tipados carregam a mesma informação — e troque os setters por chave por um único setter que aceita um partial. Cada campo de primeiro nível continua sendo seu próprio parâmetro de query.

Como ficam as outras opções

nuqs não é a única alternativa. O mesmo trabalho — estado tipado na query string — também é coberto pelos próprios roteadores e por bibliotecas mais antigas, cada uma com seu compromisso.

BibliotecaConfiguraçãoObjetos aninhados e datasTamanhoEscolha quando
state-in-urlNenhuma — importe o hookPreservados automaticamente, com tipos~2,9 KB gzip, zero depsVocê quer um objeto tipado sem configuração no Next.js, React Router ou Remix
nuqsComponente adaptador, parser por chaveParser JSON mais seu próprio validador~6,7 KB gzip, uma depVocê quer cada valor como seu próprio query param legível
TanStack RoutervalidateSearch em cada rotaJSON-first para objetos e arrays; datas com serialização própriaEmbutido no roteadorVocê está no TanStack Router — use o que ele traz
use-query-paramsProvider mais adaptador de roteador, config por parâmetroPor um tipo de parâmetro JSON, tipagem frouxa~4,4 KB gzip mais serialize-query-paramsUma base de código já construída sobre ele
useSearchParamsNenhuma — embutido no roteadorSó strings — parsing, tipos e padrões por sua conta0 KBUm ou dois parâmetros planos de string, sem biblioteca

Perguntas frequentes

state-in-url é uma boa alternativa ao nuqs?
Sim, quando você quer um objeto tipado inteiro na URL sem configuração: sem componente adaptador, sem parsers por chave, e com objetos aninhados e datas preservados automaticamente. nuqs continua sendo a melhor escolha se você quer cada valor como seu próprio query param legível ou está no TanStack Router.
Qual é menor, state-in-url ou nuqs?
Medido com esbuild (minify + gzip, import da biblioteca inteira) em agosto de 2026: state-in-url tem ~2,9 KB e zero dependências; nuqs 2.10.1 tem ~6,7 KB e uma dependência. Importar um subconjunto reduz as duas.
state-in-url precisa de adaptador ou provider?
Não. Cada roteador tem seu próprio entry point — importe o hook correspondente, passe um objeto de estado padrão e funciona. Não há componente adaptador envolvendo o app nem provider de contexto para configurar.
É difícil migrar do nuqs para o state-in-url?
Normalmente não: junte as chaves de uma feature num único objeto de estado padrão, remova as declarações de parser e troque os setters por chave por um único setter com um partial. Cada campo de primeiro nível continua sendo seu próprio parâmetro de query.
E os search params do TanStack Router?
Se você está no TanStack Router, use o que ele traz: search params JSON-first validados por rota com validateSearch. state-in-url e nuqs importam quando seu roteador é Next.js, React Router ou Remix, onde não há search params tipados embutidos.