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
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:
- Um objeto
URLSearchParams(ou uma instância similar) que representa os parâmetros de consulta atuais. - Uma função
setSearchParamspara 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 decategoria, 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 usandoparseInt(). É crucial sempre fornecer um fallback ('1') e a base numérica (10) para evitarNaN. - A função
handlePaginaChangeatualiza o parâmetropaginana 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
useDebouncehook 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 quandodebouncedTermoBuscamuda. - Um
useEffectadicional 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()ouparseFloat(). Sempre forneça a base (radix) paraparseInt().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 eappend()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
URLSearchParamsvazia 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
setSearchParamsquando houver uma mudança real nos parâmetros que você deseja persistir. - Memoização: Use
React.memo,useMemoeuseCallbackpara otimizar componentes e funções que dependem dossearchParamsou da funçãosetSearchParams.
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
- React Router Docs - useSearchParams
- MDN Web Docs - URLSearchParams
- Smashing Magazine - URL State Management in React
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.
Sobre Marcos Costa
Desenvolvedor backend com foco em arquitetura de software, automação e produtos digitais.
Ver mais artigos