Por que Sincronizar Estado com a URL?
Quando você trabalha com filtros, paginação ou qualquer tipo de estado que afete a visualização de dados em uma aplicação React, uma decisão fundamental é: onde armazenar esse estado? A resposta mais sofisticada não é apenas no estado local do componente — é na URL.
Sincronizar estado com a URL resolve problemas reais. Se um usuário aplica um conjunto de filtros em sua aplicação e depois compartilha o link com um colega, espera-se que esse colega veja exatamente a mesma página com os mesmos filtros aplicados. Além disso, o botão voltar do navegador funcionará intuitivamente, permitindo navegar pelo histórico de estados anteriores. Sem essa sincronização, você força o usuário a refazer suas ações toda vez que recarrega a página ou clica em voltar.
A URL é um banco de dados público e acessível. Ela persiste através de recarregamentos, funciona com favoritos, é compartilhável e integra-se perfeitamente com o comportamento natural do navegador. Por essas razões, usar useSearchParams do React Router para gerenciar filtros e paginação é uma prática profissional essencial.
useSearchParams: Conceito e Funcionamento
O que é useSearchParams?
useSearchParams é um hook fornecido pelo React Router que permite ler e modificar os parâmetros de consulta (query string) da URL. Se sua URL é https://app.com/produtos?categoria=eletrônicos&pagina=2, useSearchParams acessa e manipula aquela parte ?categoria=eletrônicos&pagina=2.
Esse hook retorna um array com dois elementos: um objeto URLSearchParams contendo os parâmetros atuais, e uma função para atualizar esses parâmetros. Diferentemente de useState, essas alterações também atualizam a URL do navegador, mantendo tudo sincronizado.
Diferença entre useState e useSearchParams
Imagine que você quer armazenar um filtro de categoria. Com useState, você faz:
const [categoria, setCategoria] = useState('');
Funciona, mas quando você recarrega a página, o estado volta ao padrão. A URL permanece https://app.com/produtos sem qualquer informação sobre a seleção anterior. Com useSearchParams, você toma uma decisão diferente:
const [searchParams, setSearchParams] = useSearchParams();
const categoria = searchParams.get('categoria') || '';
Agora, quando o usuário muda de categoria, a URL muda para https://app.com/produtos?categoria=eletrônicos. Se ele recarrega a página ou volta a ela depois, o filtro persiste porque está armazenado na URL.
Implementando Filtros com Sincronização de URL
Exemplo Prático: Listagem de Produtos com Filtros
Vamos construir uma página de produtos com filtros por categoria e preço, onde tudo está sincronizado com a URL. Este é um cenário real que você encontrará em projetos profissionais.
import { useSearchParams } from 'react-router-dom';
import { useState, useEffect } from 'react';
function ProdutosList() {
const [searchParams, setSearchParams] = useSearchParams();
const [produtos, setProdutos] = useState([]);
const [carregando, setCarregando] = useState(false);
// Obtém os valores atuais da URL, ou usa valores padrão
const categoria = searchParams.get('categoria') || '';
const precoMin = searchParams.get('precoMin') || '';
const precoMax = searchParams.get('precoMax') || '';
const pagina = parseInt(searchParams.get('pagina') || '1', 10);
// Busca produtos quando os filtros mudam
useEffect(() => {
setCarregando(true);
// Simula uma chamada à API
const params = new URLSearchParams({
...(categoria && { categoria }),
...(precoMin && { precoMin }),
...(precoMax && { precoMax }),
pagina,
limite: 10
});
fetch(`/api/produtos?${params}`)
.then(res => res.json())
.then(data => setProdutos(data))
.catch(err => console.error(err))
.finally(() => setCarregando(false));
}, [categoria, precoMin, precoMax, pagina]);
// Atualiza um filtro na URL
const handleFiltroChange = (chave, valor) => {
const novoParams = new URLSearchParams(searchParams);
if (valor === '' || valor === null) {
novoParams.delete(chave);
} else {
novoParams.set(chave, valor);
}
// Reseta para página 1 quando filtro muda
novoParams.set('pagina', '1');
setSearchParams(novoParams);
};
// Muda de página
const handleMudarPagina = (novaPagina) => {
const novoParams = new URLSearchParams(searchParams);
novoParams.set('pagina', novaPagina);
setSearchParams(novoParams);
};
return (
<div className="container">
<section className="filtros">
<h2>Filtros</h2>
<label>
Categoria:
<select
value={categoria}
onChange={(e) => handleFiltroChange('categoria', e.target.value)}
>
<option value="">Todas</option>
<option value="eletrônicos">Eletrônicos</option>
<option value="roupas">Roupas</option>
<option value="livros">Livros</option>
</select>
</label>
<label>
Preço Mínimo:
<input
type="number"
value={precoMin}
onChange={(e) => handleFiltroChange('precoMin', e.target.value)}
placeholder="0"
/>
</label>
<label>
Preço Máximo:
<input
type="number"
value={precoMax}
onChange={(e) => handleFiltroChange('precoMax', e.target.value)}
placeholder="1000"
/>
</label>
<button onClick={() => {
const novoParams = new URLSearchParams();
novoParams.set('pagina', '1');
setSearchParams(novoParams);
}}>
Limpar Filtros
</button>
</section>
<section className="produtos">
{carregando ? (
<p>Carregando...</p>
) : produtos.length === 0 ? (
<p>Nenhum produto encontrado</p>
) : (
<>
<ul>
{produtos.map(produto => (
<li key={produto.id}>
<h3>{produto.nome}</h3>
<p>R$ {produto.preco.toFixed(2)}</p>
<span className="categoria">{produto.categoria}</span>
</li>
))}
</ul>
<div className="paginacao">
<button
disabled={pagina === 1}
onClick={() => handleMudarPagina(pagina - 1)}
>
Anterior
</button>
<span>Página {pagina}</span>
<button
onClick={() => handleMudarPagina(pagina + 1)}
>
Próxima
</button>
</div>
</>
)}
</section>
</div>
);
}
export default ProdutosList;
Como o Código Funciona
A lógica é simples mas poderosa. Cada filtro lê seu valor da URL através de searchParams.get(). Quando o usuário muda um filtro, handleFiltroChange cria uma cópia dos parâmetros atuais, modifica o que mudou e chama setSearchParams para atualizar a URL. O useEffect observa todas essas mudanças e dispara uma nova busca na API.
Observe que ao limpar filtros, reseta-se a página para 1. Isso previne confusão onde um usuário está na página 5, limpa os filtros e não vê nada porque a página 5 pode não existir com os novos filtros. É um detalhe que profissionais sempre implementam.
Padrões Avançados e Boas Práticas
Sincronização com Estados Derivados
Em aplicações mais complexas, você pode ter múltiplas fontes de estado: a URL, o estado local, dados em cache. A chave é estabelecer a URL como a fonte única de verdade. Nunca replique o valor da URL em useState — sempre leia diretamente de searchParams.
// ❌ Errado: duplica dados
const [searchParams] = useSearchParams();
const [categoria, setCategoria] = useState(searchParams.get('categoria'));
// ✅ Correto: uma fonte de verdade
const [searchParams] = useSearchParams();
const categoria = searchParams.get('categoria');
Debouncing para Filtros em Tempo Real
Se você tem um campo de busca por texto que deveria atualizar a URL a cada digitação, sem debouncing a URL será reescrita dezenas de vezes por segundo. Use useEffect com debouncing:
import { useSearchParams } from 'react-router-dom';
import { useEffect, useRef } from 'react';
function BuscaProdutos() {
const [searchParams, setSearchParams] = useSearchParams();
const [busca, setBusca] = useState(searchParams.get('q') || '');
const timerRef = useRef(null);
useEffect(() => {
// Limpa timer anterior
if (timerRef.current) {
clearTimeout(timerRef.current);
}
// Aguarda 500ms após o usuário parar de digitar
timerRef.current = setTimeout(() => {
const novoParams = new URLSearchParams(searchParams);
if (busca.trim()) {
novoParams.set('q', busca);
} else {
novoParams.delete('q');
}
setSearchParams(novoParams);
}, 500);
return () => clearTimeout(timerRef.current);
}, [busca, searchParams, setSearchParams]);
return (
<input
type="text"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar produtos..."
/>
);
}
Preservando Filtros ao Navegar
Quando um usuário clica em um produto para ver detalhes e depois volta, você quer que os filtros anteriores estejam lá. Isso funciona naturalmente com useSearchParams porque a URL é preservada no histórico do navegador. O botão voltar restaura a URL anterior e seus parâmetros automaticamente.
Para ir além, você pode criar um helper que sanitiza e valida parâmetros:
function useFiltrosValidos(opcoesValidas) {
const [searchParams, setSearchParams] = useSearchParams();
const filtrosSanitizados = Object.fromEntries(
Array.from(searchParams.entries()).filter(([chave, valor]) => {
const config = opcoesValidas[chave];
if (!config) return false;
if (config.tipo === 'enum') {
return config.valores.includes(valor);
}
if (config.tipo === 'numero') {
return !isNaN(valor) && valor >= config.min && valor <= config.max;
}
return true;
})
);
return [filtrosSanitizados, setSearchParams];
}
// Uso
const opcoesValidas = {
categoria: { tipo: 'enum', valores: ['eletrônicos', 'roupas', 'livros'] },
precoMin: { tipo: 'numero', min: 0, max: 10000 },
pagina: { tipo: 'numero', min: 1, max: Infinity }
};
const [filtros] = useFiltrosValidos(opcoesValidas);
Conclusão
Aprendemos que sincronizar estado com a URL via useSearchParams resolve problemas fundamentais de compartilhamento, persistência e navegabilidade que toda aplicação profissional deve resolver. Não é apenas uma feature técnica — é uma expectativa dos usuários modernos.
Em segundo lugar, a URL deve ser a fonte única de verdade para filtros e paginação. Nunca duplique esses valores em useState paralelo. Sempre leia de searchParams e escreva através de setSearchParams.
Por fim, proteja sua aplicação contra parâmetros inválidos e implemente padrões como debouncing para campos dinâmicos. Profissionais entendem que código que funciona no caso feliz é apenas o começo — é a robustez diante de casos extremos que faz a diferença.