Guia Completo de URL como Estado em React: useSearchParams e Sincronização de Filtros Já leu

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

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.

Referências


Artigos relacionados