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-url

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

page.tsx
// 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} />;
}
JobsList.tsx
// 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
// 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 } });
}
layout.tsx
// 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.

useJobsState — usage
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 default

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