Front-end

Gerenciando Estado na URL com React Router: Guia Prático para Filtros, Paginação e Busca

Aprenda a persistir e sincronizar o estado da sua aplicação React (filtros, paginação, busca) diretamente na URL usando o hook `useSearchParams` do React Router v6+. Melhore a experiência do usuário e a

Marcos Costa
Marcos Costa
22 de setembro de 2026 12 min de leitura
Tela dividida mostrando um navegador web com uma URL contendo parâmetros de busca, paginação e filtro, ao lado de um editor de código exibindo código React JavaScript com o hook `useSearchParams` para gerenciamento de estado na URL.

Em aplicações React modernas, a experiência do usuário é fundamental. Imagine um usuário aplicando filtros complexos, navegando por várias páginas ou realizando uma busca específica, apenas para perder todo o progresso ao recarregar a página ou tentar compartilhar o link. O gerenciamento de estado na URL resolve esse problema, tornando suas aplicações mais robustas e amigáveis.

Neste guia, vamos explorar como o React Router, com seu poderoso hook useSearchParams, nos permite persistir e sincronizar o estado da aplicação diretamente na URL, garantindo que filtros, paginação e termos de busca sejam mantidos e compartilháveis. Ao final, você será capaz de implementar essas funcionalidades, melhorando significativamente a usabilidade e a capacidade de compartilhamento dos seus produtos digitais.

Por Que Gerenciar o Estado na URL é Essencial para Aplicações React?

Manter o estado da aplicação refletido na URL é uma prática que eleva a qualidade da experiência do usuário e a robustez do software. Quando filtros, termos de busca e a página atual são persistidos na URL, a aplicação se torna:

  • Compartilhável: Usuários podem copiar e colar a URL para compartilhar o estado exato da aplicação com outras pessoas, que verão o mesmo conteúdo e filtros aplicados.
  • Bookmarkável: É possível salvar a página nos favoritos do navegador, retornando posteriormente ao mesmo estado de filtros e paginação.
  • Resiliente a Recarregamentos: Ao recarregar a página, o estado não é perdido, pois ele é lido diretamente da URL, proporcionando uma experiência mais fluida e menos frustrante.
  • Amigável para SEO: Embora o React seja uma SPA (Single Page Application), a indexação de conteúdo filtrado ou paginado pode ser beneficiada se os parâmetros forem bem estruturados na URL, especialmente em cenários de Server-Side Rendering (SSR).

Para começar a construir aplicações React robustas que aproveitam esses benefícios, é fundamental saber Como criar um projeto em react. Com uma base sólida, a implementação do gerenciamento de estado na URL se torna um passo natural para aprimorar a interação do usuário.

Desvendando o useSearchParams do React Router v6+

O React Router v6+ introduziu o hook useSearchParams como a forma recomendada e mais eficiente de ler e modificar os parâmetros de consulta (query parameters) da URL. Ele simplifica a manipulação da query string, que é a parte da URL que vem após o ? (ex: ?filtro=valor&pagina=2).

Internamente, useSearchParams se baseia na API nativa do navegador URLSearchParams (MDN Web Docs - URLSearchParams). Este hook retorna um array contendo:

  1. Um objeto URLSearchParams (ou uma instância similar) que representa os parâmetros de consulta atuais.
  2. Uma função setSearchParams para atualizar esses parâmetros, que automaticamente navega para a nova URL.

Por que useSearchParams é superior a useLocation para este fim?

Enquanto useLocation fornece acesso ao objeto de localização completo, incluindo a query string como uma string simples (location.search), useSearchParams oferece uma interface de objeto para manipular esses parâmetros de forma muito mais intuitiva. Com useSearchParams, você não precisa parsear e serializar manualmente a string da URL, o que reduz a chance de erros e torna o código mais limpo e legível. Ele é o padrão para gerenciamento de query parameters no React Router v6+ (React Router Docs - useSearchParams).

Exemplo básico de uso:

import React from 'react';
import { useSearchParams } from 'react-router-dom';

function MeuComponente() {
  const [searchParams, setSearchParams] = useSearchParams();

  // Lendo um parâmetro
  const termoBusca = searchParams.get('busca');
  console.log('Termo de busca:', termoBusca);

  // Atualizando um parâmetro
  const handleClick = () => {
    searchParams.set('categoria', 'eletronicos');
    setSearchParams(searchParams); // Atualiza a URL
  };

  return (
    <div>
      <p>Parâmetro 'busca': {termoBusca || 'Nenhum'}</p>
      <button onClick={handleClick}>Definir Categoria</button>
    </div>
  );
}

export default MeuComponente;

Implementando Filtros Dinâmicos na URL

Gerenciar filtros é um dos casos de uso mais comuns para persistir o estado na URL. Vamos criar um exemplo onde podemos filtrar produtos por categoria e status.

import React from 'react';
import { useSearchParams } from 'react-router-dom';

function FiltrosProdutos() {
  const [searchParams, setSearchParams] = useSearchParams();

  const categoriasSelecionadas = searchParams.getAll('categoria');
  const statusSelecionado = searchParams.get('status');

  const handleCategoriaChange = (e) => {
    const categoria = e.target.value;
    const novasCategorias = new Set(categoriasSelecionadas);

    if (e.target.checked) {
      novasCategorias.add(categoria);
    } else {
      novasCategorias.delete(categoria);
    }

    // Limpa todos os 'categoria' existentes e adiciona os novos
    searchParams.delete('categoria');
    novasCategorias.forEach(cat => searchParams.append('categoria', cat));
    setSearchParams(searchParams);
  };

  const handleStatusChange = (e) => {
    const status = e.target.value;
    if (status === 'todos') {
      searchParams.delete('status');
    } else {
      searchParams.set('status', status);
    }
    setSearchParams(searchParams);
  };

  return (
    <div>
      <h3>Filtrar por Categoria:</h3>
      <label>
        <input
          type="checkbox"
          value="eletronicos"
          checked={categoriasSelecionadas.includes('eletronicos')}
          onChange={handleCategoriaChange}
        />
        Eletrônicos
      </label>
      <label>
        <input
          type="checkbox"
          value="livros"
          checked={categoriasSelecionadas.includes('livros')}
          onChange={handleCategoriaChange}
        />
        Livros
      </label>

      <h3>Filtrar por Status:</h3>
      <select value={statusSelecionado || 'todos'} onChange={handleStatusChange}>
        <option value="todos">Todos</option>
        <option value="disponivel">Disponível</option>
        <option value="esgotado">Esgotado</option>
      </select>

      <p>Categorias na URL: {categoriasSelecionadas.join(', ') || 'Nenhuma'}</p>
      <p>Status na URL: {statusSelecionado || 'Todos'}</p>
    </div>
  );
}

export default FiltrosProdutos;

Neste exemplo:

  • searchParams.getAll('categoria') é usado para obter um array com todos os valores de categoria, permitindo múltiplos filtros.
  • Para atualizar múltiplos valores, primeiro deletamos todos os existentes (searchParams.delete('categoria')) e depois adicionamos os novos (searchParams.append('categoria', cat)).
  • Para um filtro de valor único como status, searchParams.set('status', status) sobrescreve o valor anterior.

Sincronizando Paginação com a URL de Forma Eficiente

A paginação é outra funcionalidade que se beneficia enormemente da persistência na URL. Isso permite que o usuário navegue entre as páginas e que o estado da página seja mantido e compartilhável.

import React from 'react';
import { useSearchParams } from 'react-router-dom';

const TOTAL_ITENS = 100;
const ITENS_POR_PAGINA = 10;

function Paginacao() {
  const [searchParams, setSearchParams] = useSearchParams();

  // Converte o parâmetro 'pagina' para número, com fallback para 1
  const paginaAtual = parseInt(searchParams.get('pagina') || '1', 10);
  const totalPaginas = Math.ceil(TOTAL_ITENS / ITENS_POR_PAGINA);

  const handlePaginaChange = (novaPagina) => {
    if (novaPagina < 1 || novaPagina > totalPaginas) return;

    searchParams.set('pagina', novaPagina.toString());
    setSearchParams(searchParams);
  };

  return (
    <div>
      <h3>Paginação:</h3>
      <p>Página Atual: {paginaAtual}</p>
      <p>Total de Páginas: {totalPaginas}</p>
      <div>
        <button
          onClick={() => handlePaginaChange(paginaAtual - 1)}
          disabled={paginaAtual === 1}
        >
          Anterior
        </button>
        {[...Array(totalPaginas)].map((_, index) => (
          <button
            key={index + 1}
            onClick={() => handlePaginaChange(index + 1)}
            disabled={paginaAtual === index + 1}
            style={{ margin: '0 5px' }}
          >
            {index + 1}
          </button>
        ))}
        <button
          onClick={() => handlePaginaChange(paginaAtual + 1)}
          disabled={paginaAtual === totalPaginas}
        >
          Próxima
        </button>
      </div>
    </div>
  );
}

export default Paginacao;

Neste componente de paginação:

  • O parâmetro pagina é lido da URL e convertido para um número inteiro usando parseInt(). É crucial sempre fornecer um fallback ('1') e a base numérica (10) para evitar NaN.
  • A função handlePaginaChange atualiza o parâmetro pagina na URL, convertendo o número de volta para string antes de definir.

Persistindo Termos de Busca e Otimizando com Debouncing

Campos de busca são interativos e podem gerar muitas atualizações na URL se não forem otimizados. Para evitar re-renderizações excessivas e chamadas de API desnecessárias, o debouncing é essencial.

O debouncing atrasa a execução de uma função até que um certo tempo tenha passado desde a última vez que ela foi chamada. No contexto de uma busca, isso significa que a URL só será atualizada (e, consequentemente, a busca será disparada) depois que o usuário parar de digitar por um breve período.

import React, { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';

// Hook de debouncing customizado
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

function CampoBusca() {
  const [searchParams, setSearchParams] = useSearchParams();
  const termoBuscaURL = searchParams.get('q') || '';

  const [termoBuscaLocal, setTermoBuscaLocal] = useState(termoBuscaURL);
  const debouncedTermoBusca = useDebounce(termoBuscaLocal, 500); // 500ms de delay

  // Sincroniza o estado local com a URL quando a URL muda (ex: usuário compartilha link)
  useEffect(() => {
    if (termoBuscaURL !== termoBuscaLocal) {
      setTermoBuscaLocal(termoBuscaURL);
    }
  }, [termoBuscaURL]);

  // Atualiza a URL apenas quando o termo debounced muda
  useEffect(() => {
    if (debouncedTermoBusca) {
      searchParams.set('q', debouncedTermoBusca);
    } else {
      searchParams.delete('q');
    }
    setSearchParams(searchParams);
  }, [debouncedTermoBusca, setSearchParams, searchParams]);

  const handleChange = (e) => {
    setTermoBuscaLocal(e.target.value);
  };

  return (
    <div>
      <h3>Buscar:</h3>
      <input
        type="text"
        placeholder="Digite para buscar..."
        value={termoBuscaLocal}
        onChange={handleChange}
        style={{ width: '300px', padding: '8px' }}
      />
      <p>Termo de busca na URL (debounced): {searchParams.get('q') || 'Nenhum'}</p>
      <p>Termo de busca local (instantâneo): {termoBuscaLocal || 'Nenhum'}</p>
    </div>
  );
}

export default CampoBusca;

Neste exemplo:

  • Um useDebounce hook customizado é implementado para atrasar a atualização do termo de busca na URL.
  • O estado local (termoBuscaLocal) é atualizado instantaneamente, mas a URL (searchParams.set('q', ...) ) só é atualizada quando debouncedTermoBusca muda.
  • Um useEffect adicional garante que se a URL for alterada externamente (ex: usuário cola um link com um termo de busca), o estado local do input seja sincronizado.

Boas Práticas: Conversão de Tipos, Limpeza e Performance

Ao trabalhar com parâmetros de URL, é crucial seguir algumas boas práticas para garantir a robustez, a usabilidade e a performance da sua aplicação.

Conversão de Tipos

Todos os valores lidos da URL são strings. Frequentemente, você precisará convertê-los para outros tipos (números, booleanos, arrays) e vice-versa ao escrevê-los na URL.

  • String para Número: Use parseInt() ou parseFloat(). Sempre forneça a base (radix) para parseInt().
    const pagina = parseInt(searchParams.get('pagina') || '1', 10);
    const limite = parseInt(searchParams.get('limite') || '10', 10);
  • String para Booleano: A string ‘true’ não é automaticamente um booleano true. Compare explicitamente.
    const ativo = searchParams.get('ativo') === 'true';
  • Número/Booleano para String: Use .toString().
    searchParams.set('pagina', novaPagina.toString());
    searchParams.set('ativo', true.toString()); // resultará em '?ativo=true'
  • Arrays: Para múltiplos valores, use getAll() para ler e append() para escrever. Para arrays de objetos, considere serializar para JSON e codificar/decodificar a URL, embora isso possa tornar a URL menos legível.

Limpando ou Redefinindo Parâmetros da URL

  • Remover um único parâmetro: Use searchParams.delete('chave').
    searchParams.delete('status');
    setSearchParams(searchParams);
  • Remover todos os valores de um parâmetro com múltiplos valores: Também searchParams.delete('chave').
    searchParams.delete('categoria'); // Remove todas as ocorrências de 'categoria'
    setSearchParams(searchParams);
  • Redefinir todos os parâmetros: Crie uma nova instância de URLSearchParams vazia ou com valores padrão.
    setSearchParams(new URLSearchParams()); // Limpa todos os parâmetros
    // Ou para redefinir para um estado inicial específico:
    // setSearchParams(new URLSearchParams({ pagina: '1', limite: '10' }));

Considerações sobre Performance e Re-renderizações

Manipular a URL pode causar re-renderizações nos componentes que usam useSearchParams. Para otimizar:

  • Debouncing: Como visto no exemplo de busca, é crucial para inputs em tempo real.
  • Evite atualizações desnecessárias: Só chame setSearchParams quando houver uma mudança real nos parâmetros que você deseja persistir.
  • Memoização: Use React.memo, useMemo e useCallback para otimizar componentes e funções que dependem dos searchParams ou da função setSearchParams.

Para aprimorar ainda mais a performance e a experiência de desenvolvimento, conhecer Conheça 5 bibliotecas do react que vão facilitar seu trabalho pode ser muito útil, oferecendo ferramentas que complementam o gerenciamento de estado na URL.

Testando Componentes que Interagem com a URL

Testar componentes que utilizam useSearchParams é fundamental para garantir que sua aplicação se comporte conforme o esperado. Para isso, você precisará simular o ambiente do React Router em seus testes.

Ferramentas como @testing-library/react e jest são ideais. O truque é envolver o componente a ser testado em um BrowserRouter (ou MemoryRouter para testes) e, se necessário, mockar o useSearchParams ou passar initialEntries para o MemoryRouter para simular URLs específicas.

Exemplo de teste para o componente FiltrosProdutos:

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import FiltrosProdutos from './FiltrosProdutos'; // Assumindo que o componente está em './FiltrosProdutos.js'

describe('FiltrosProdutos', () => {
  it('deve aplicar e remover filtros de categoria na URL', () => {
    render(
      <MemoryRouter initialEntries={['/produtos?categoria=eletronicos']}>
        <FiltrosProdutos />
      </MemoryRouter>
    );

    // Verifica se 'Eletrônicos' está inicialmente marcado
    const checkboxEletronicos = screen.getByLabelText('Eletrônicos');
    expect(checkboxEletronicos).toBeChecked();

    // Marca 'Livros'
    const checkboxLivros = screen.getByLabelText('Livros');
    fireEvent.click(checkboxLivros);

    // Verifica se a URL foi atualizada com ambas as categorias
    // (Em um teste real, você verificaria a URL ou o mock de setSearchParams)
    // Para este exemplo, vamos verificar o texto exibido no componente
    expect(screen.getByText(/Categorias na URL: eletronicos, livros/i)).toBeInTheDocument();

    // Desmarca 'Eletrônicos'
    fireEvent.click(checkboxEletronicos);
    expect(checkboxEletronicos).not.toBeChecked();
    expect(screen.getByText(/Categorias na URL: livros/i)).toBeInTheDocument();
  });

  it('deve aplicar e mudar o filtro de status na URL', () => {
    render(
      <MemoryRouter initialEntries={['/produtos?status=disponivel']}>
        <FiltrosProdutos />
      </MemoryRouter>
    );

    const selectStatus = screen.getByRole('combobox');
    expect(selectStatus).toHaveValue('disponivel');

    // Muda o status para 'esgotado'
    fireEvent.change(selectStatus, { target: { value: 'esgotado' } });
    expect(selectStatus).toHaveValue('esgotado');
    expect(screen.getByText(/Status na URL: esgotado/i)).toBeInTheDocument();

    // Muda o status para 'todos' (limpa o parâmetro)
    fireEvent.change(selectStatus, { target: { value: 'todos' } });
    expect(selectStatus).toHaveValue('todos');
    expect(screen.getByText(/Status na URL: Todos/i)).toBeInTheDocument();
  });
});

Para testes mais avançados, você pode usar jest.mock('react-router-dom', ...) para ter controle total sobre o que useSearchParams retorna e como setSearchParams se comporta. Garantir a qualidade do software através de testes é um pilar fundamental do desenvolvimento moderno. Para aprofundar-se, confira O que é Qualidade de Software e por que ela é essencial no desenvolvimento moderno?.

Conclusão

Gerenciar o estado da aplicação diretamente na URL com useSearchParams do React Router v6+ é uma técnica poderosa que melhora a experiência do usuário, a compartilhabilidade e a resiliência de suas aplicações React. Ao dominar a leitura, escrita e manipulação de query parameters, você pode criar interfaces mais intuitivas e robustas, que respondem de forma inteligente às interações do usuário e ao contexto da navegação. Lembre-se das boas práticas de conversão de tipos, limpeza de parâmetros e otimização de performance para construir aplicações de alta qualidade.

Referências

FAQ

Qual a diferença entre useLocation e useSearchParams para gerenciar query strings?

Enquanto useLocation fornece acesso ao objeto de localização completo (incluindo a query string como uma string simples em location.search), useSearchParams é um hook específico do React Router v6+ que retorna um objeto URLSearchParams e uma função para atualizá-lo. Isso torna a manipulação de query parameters muito mais fácil, orientada a objetos e é a abordagem recomendada para essa finalidade.

Como posso lidar com múltiplos valores para um mesmo parâmetro de filtro na URL?

A API URLSearchParams permite adicionar múltiplos valores para a mesma chave (ex: ?categoria=eletronicos&categoria=livros). Você pode usar searchParams.getAll('chave') para obter um array com todos os valores e searchParams.append('chave', 'valor') para adicionar novos valores sem sobrescrever os existentes. Para remover ou atualizar, você pode primeiro usar searchParams.delete('chave') para limpar todas as ocorrências e depois adicionar os valores desejados com append().

É necessário usar um gerenciador de estado global (Redux, Zustand) junto com o estado da URL?

Não necessariamente. Para filtros, paginação e busca que afetam diretamente a exibição da URL e precisam ser compartilháveis, useSearchParams é frequentemente suficiente e a abordagem mais direta. Um gerenciador de estado global pode ser útil para estados mais complexos que não precisam ser refletidos na URL, que são compartilhados por muitos componentes de forma mais profunda, ou para gerenciar o estado de dados da aplicação que são buscados com base nos parâmetros da URL, mas não são os próprios parâmetros.

Marcos Costa

Sobre Marcos Costa

Desenvolvedor backend com foco em arquitetura de software, automação e produtos digitais.

Ver mais artigos