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-urlstate-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-url | nuqs |
|---|---|---|
| Configuração | Nenhuma — importe o hook e pronto | Um componente adaptador envolve o app |
| Formato do estado | Um objeto tipado, como React.useState | Valores por chave, com um parser declarado para cada uma |
| Reuso entre componentes | Envolva o hook uma vez — cada componente compartilha o estado, sem props | Você extrai seu próprio hook em volta do mapa de parsers |
| Objetos e arrays aninhados | Nativo — estrutura e tipos preservados | Parser JSON mais seu próprio validador |
| Datas | Preservadas automaticamente | Parser embutido, declarado por chave |
| Tamanho, import completo | ~2,9 KB gzip | ~6,7 KB gzip |
| Dependências em runtime | Nenhuma | Uma |
| Roteadores | Next.js, React Router v6/v7, Remix, helpers para JS puro | Next.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
import { NuqsAdapter } from 'nuqs/adapters/next/app';
export default function RootLayout({ children }) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}'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 })}
/>
);
};'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.
| Biblioteca | Configuração | Objetos aninhados e datas | Tamanho | Escolha quando |
|---|---|---|---|---|
| state-in-url | Nenhuma — importe o hook | Preservados automaticamente, com tipos | ~2,9 KB gzip, zero deps | Você quer um objeto tipado sem configuração no Next.js, React Router ou Remix |
| nuqs | Componente adaptador, parser por chave | Parser JSON mais seu próprio validador | ~6,7 KB gzip, uma dep | Você quer cada valor como seu próprio query param legível |
| TanStack Router | validateSearch em cada rota | JSON-first para objetos e arrays; datas com serialização própria | Embutido no roteador | Você está no TanStack Router — use o que ele traz |
| use-query-params | Provider mais adaptador de roteador, config por parâmetro | Por um tipo de parâmetro JSON, tipagem frouxa | ~4,4 KB gzip mais serialize-query-params | Uma base de código já construída sobre ele |
| useSearchParams | Nenhuma — embutido no roteador | Só strings — parsing, tipos e padrões por sua conta | 0 KB | Um 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.
