Estado tipado na URL para React e Next.js — como useState
useUrlState é o estado React que se grava na query string. Objetos, arrays e datas mantêm seus tipos, cada estado é um link compartilhável, sobrevive a recarregamentos e o botão voltar funciona — sem providers, sem limite de Suspense, sem boilerplate.
- ~2 KB em gzip
- zero dependências
- TypeScript-first
- Next.js / react-router / Remix / Astro
- MIT
npm i state-in-urlGerenciamento de estado na URL no Next.js App Router
state-in-url mantém estado tipado na query string no Next.js 14, 15 e 16: um hook useUrlState por feature, sem adaptador, sem provider, sem limite de Suspense. Esta página cobre o que é específico do App Router — Server Components, pré-renderização, layouts e histórico.
A demonstração ao vivo na página inicial roda no Next.js 16.
Encaminhe searchParams da página no servidor
Uma página Server Component recebe searchParams — uma Promise desde o Next.js 15. Aguarde-a com await e passe o objeto para o componente de cliente, que o entrega ao hook. A primeira renderização no servidor então mostra os valores da URL em vez dos padrões, sem flash e sem aviso de hidratação.
// app/jobs/page.tsx (Server Component)
import { JobsList } from './JobsList';
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
// A Promise since Next.js 15; a plain object in 14
return <JobsList searchParams={await searchParams} />;
}// app/jobs/JobsList.tsx
'use client';
import { useUrlState } from 'state-in-url/next';
// Outside the component: sharing is keyed by object identity
const JOBS_STATE = { q: '', page: 1, remote: false, tags: [] as string[] };
export function JobsList({ searchParams }: { searchParams: object }) {
const { urlState, setUrl } = useUrlState(JOBS_STATE, { searchParams });
return (
<input
value={urlState.q}
onChange={(ev) => setUrl({ q: ev.target.value, page: 1 })}
/>
);
}Sem limite de Suspense, pré-renderização mantida
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. Uma página pré-renderizada ainda renderiza os padrões, porque não existe query string em tempo de build — renderize a rota dinamicamente quando um link compartilhado precisar estar certo já na primeira pintura.
Layouts: decodifique a query string a partir de um header
Layouts de servidor nunca recebem searchParams. Copie a query string para um header da requisição no proxy.ts (middleware.ts ainda funciona como alias descontinuado) e decodifique-a no layout com decodeState e o mesmo objeto de estado padrão — o resultado é tipado exatamente como o urlState no cliente.
// proxy.ts (middleware.ts before Next.js 16)
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export function proxy(request: NextRequest) {
const sp = (request.url.includes('_next') ? '' : request.url).split('?')[1] ?? '';
const headers = new Headers(request.headers);
headers.set('searchParams', sp);
return NextResponse.next({ request: { headers } });
}// app/jobs/layout.tsx (Server Component)
import { headers } from 'next/headers';
import { decodeState } from 'state-in-url/encodeState';
import { JOBS_STATE } from './jobsState';
export default async function Layout({ children }: { children: React.ReactNode }) {
const sp = (await headers()).get('searchParams') ?? '';
const initial = decodeState(sp, JOBS_STATE); // typed like urlState
return <>{/* use `initial` */}{children}</>;
}Histórico, atualizações shallow e scroll
setUrl substitui a entrada atual do histórico por padrão, então digitar não acumula entradas; passe replace: false para adicionar uma. As atualizações passam pela History API — sem ida ao servidor e sem requisição _rsc a cada tecla. Passe useHistory: false para usar o roteador do Next.js, quando o servidor deve re-renderizar a cada mudança. scroll é false por padrão.
Inputs rápidos: renderize agora, grave a URL depois
Para campos de texto e sliders, atualize com setState a cada mudança e chame setUrl() sem argumentos no blur ou depois de um debounce. O componente re-renderiza imediatamente; a URL é gravada uma vez, com diff baseado em conteúdo, então chamá-lo repetidamente é seguro.
const { urlState, setState, setUrl } = useJobsState();
// Render now, write the URL once the field is left
<input
value={urlState.q}
onChange={(ev) => setState({ q: ev.target.value })}
onBlur={() => setUrl()}
/>
setUrl({ page: 2 }); // replaces the history entry (default)
setUrl({ page: 2 }, { replace: false }); // pushes a new one — Back returns to page 1
setUrl({ page: 2 }, { scroll: true }); // scroll to top, off by defaultLeia a comparação completa — a mesma feature nas duas e como migrar
Estado na URL no Next.js — perguntas frequentes
- Como manter estado na URL no Next.js App Router?
- Defina um objeto de estado padrão fora do componente, envolva o useUrlState de state-in-url/next num hook pequeno e chame esse hook em qualquer componente de cliente. urlState é o valor atual tipado e setUrl grava um partial na query string. Passe a prop searchParams da página para que a renderização no servidor já esteja correta.
- useSearchParams precisa de limite de Suspense? E o state-in-url?
- O useSearchParams do Next faz uma rota renderizada estaticamente passar a renderizar no cliente até o limite de Suspense mais próximo, e o build falha sem um. state-in-url nunca o chama: lê searchParams no servidor e window.location no cliente, então nenhum limite é necessário e a pré-renderização, PPR incluído, é mantida.
- Como ler o estado da URL em um Server Component?
- Páginas o recebem como a prop searchParams — aguarde-a com await e encaminhe ao hook no cliente ou decodifique no servidor com decodeState e o mesmo objeto padrão. Layouts não recebem searchParams; exponha a query string por um header definido no proxy.ts e decodifique esse header.
- Atualizar a URL re-renderiza a página no servidor?
- Por padrão, não. setUrl atualiza pela History API, então nada é buscado e nenhuma requisição _rsc é feita. Quando o servidor deve ver o novo estado — digamos, para buscar de novo uma lista num Server Component — passe useHistory: false para que as atualizações passem pelo roteador do Next.js e a rota re-renderize.
- state-in-url é uma alternativa ao nuqs para Next.js?
- Sim. Ambos mantêm estado tipado na query string; state-in-url recebe um objeto com valores aninhados e datas preservados, não precisa de componente adaptador nem de parser por chave, e nunca toca no useSearchParams. nuqs encaixa melhor quando cada valor deve ser seu próprio query param legível. Veja a comparação completa.
- Quais versões do Next.js são suportadas?
- Next.js 14, 15 e 16 no App Router, incluindo o searchParams assíncrono introduzido no 15 e cacheComponents com PPR no 16. Outras configurações podem usar os helpers encodeState e decodeState, independentes de framework, com o roteador que preferirem.
