<h2>Por que Sincronizar Estado com a URL?</h2>
<p>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.</p>
<p>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.</p>
<p>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 <code>useSearchParams</code> do React Router para gerenciar filtros e paginação é uma prática profissional essencial.</p>
<h2>useSearchParams: Conceito e Funcionamento</h2>
<h3>O que é useSearchParams?</h3>
<p><code>useSearchParams</code> é um hook fornecido pelo React Router que permite ler e modificar os parâmetros de consulta (query string) da URL. Se sua URL é <code>https://app.com/produtos?categoria=eletrônicos&pagina=2</code>, <code>useSearchParams</code> acessa e manipula aquela parte <code>?categoria=eletrônicos&pagina=2</code>.</p>
<p>Esse hook retorna um array com dois elementos: um objeto <code>URLSearchParams</code> contendo os parâmetros atuais, e uma função para atualizar esses parâmetros. Diferentemente de <code>useState</code>, essas alterações também atualizam a URL do navegador, mantendo tudo sincronizado.</p>
<h3>Diferença entre useState e useSearchParams</h3>
<p>Imagine que você quer armazenar um filtro de categoria. Com <code>useState</code>, você faz:</p>
<pre><code class="language-javascript">const [categoria, setCategoria] = useState('');</code></pre>
<p>Funciona, mas quando você recarrega a página, o estado volta ao padrão. A URL permanece <code>https://app.com/produtos</code> sem qualquer informação sobre a seleção anterior. Com <code>useSearchParams</code>, você toma uma decisão diferente:</p>
<pre><code class="language-javascript">const [searchParams, setSearchParams] = useSearchParams();
const categoria = searchParams.get('categoria') || '';</code></pre>
<p>Agora, quando o usuário muda de categoria, a URL muda para <code>https://app.com/produtos?categoria=eletrônicos</code>. Se ele recarrega a página ou volta a ela depois, o filtro persiste porque está armazenado na URL.</p>
<h2>Implementando Filtros com Sincronização de URL</h2>
<h3>Exemplo Prático: Listagem de Produtos com Filtros</h3>
<p>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.</p>
<pre><code class="language-javascript">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;</code></pre>
<h3>Como o Código Funciona</h3>
<p>A lógica é simples mas poderosa. Cada filtro lê seu valor da URL através de <code>searchParams.get()</code>. Quando o usuário muda um filtro, <code>handleFiltroChange</code> cria uma cópia dos parâmetros atuais, modifica o que mudou e chama <code>setSearchParams</code> para atualizar a URL. O <code>useEffect</code> observa todas essas mudanças e dispara uma nova busca na API.</p>
<p>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.</p>
<h2>Padrões Avançados e Boas Práticas</h2>
<h3>Sincronização com Estados Derivados</h3>
<p>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 <code>useState</code> — sempre leia diretamente de <code>searchParams</code>.</p>
<pre><code class="language-javascript"></code></pre>
<h3>Debouncing para Filtros em Tempo Real</h3>
<p>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 <code>useEffect</code> com debouncing:</p>
<pre><code class="language-javascript">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..."
/>
);
}</code></pre>
<h3>Preservando Filtros ao Navegar</h3>
<p>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 <code>useSearchParams</code> porque a URL é preservada no histórico do navegador. O botão voltar restaura a URL anterior e seus parâmetros automaticamente.</p>
<p>Para ir além, você pode criar um helper que sanitiza e valida parâmetros:</p>
<pre><code class="language-javascript">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);</code></pre>
<h2>Conclusão</h2>
<p>Aprendemos que <strong>sincronizar estado com a URL via <code>useSearchParams</code> resolve problemas fundamentais de compartilhamento, persistência e navegabilidade</strong> que toda aplicação profissional deve resolver. Não é apenas uma feature técnica — é uma expectativa dos usuários modernos.</p>
<p>Em segundo lugar, <strong>a URL deve ser a fonte única de verdade para filtros e paginação</strong>. Nunca duplique esses valores em <code>useState</code> paralelo. Sempre leia de <code>searchParams</code> e escreva através de <code>setSearchParams</code>.</p>
<p>Por fim, <strong>proteja sua aplicação contra parâmetros inválidos e implemente padrões como debouncing para campos dinâmicos</strong>. 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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://reactrouter.com/en/main/hooks/use-search-params" target="_blank" rel="noopener noreferrer">React Router: useSearchParams Documentation</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams" target="_blank" rel="noopener noreferrer">MDN Web Docs: URLSearchParams API</a></li>
<li><a href="https://reactrouter.com/en/main/start/overview" target="_blank" rel="noopener noreferrer">React Router: Location State Pattern</a></li>
<li><a href="https://web.dev/bfcache/" target="_blank" rel="noopener noreferrer">Web.dev: Sharing URLs with state</a></li>
</ul>
<p><!-- FIM --></p>