Se você já ouviu a sigla SERP, entenderá imediatamente o sentido deste post.
Venha comigo na minha jornada para refatorar uma página SERP e seu pesadelo de filtros.
Por que páginas SERP são importantes?
Páginas SERP são comuns em lojas virtuais, onde pesquisar algo eficientemente é a funcionalidade mais importante.
Frequentemente, é a parte mais complexa do site, cheia de filtros, lógica de UI e bugs.
Também frequentemente, a maioria dos bugs é causada por escolhas ruins de design. É nisso que estou investindo meu tempo esta semana. Quero encontrar uma boa arquitetura para esses filtros.
O estado intermediário: um erro comum
Filtros em páginas SERP sempre são representados por uma URL com vários query params. Historicamente isso é feito para facilitar o compartilhamento do link.
No meu caso, os filtros têm uma camada intermediária de estado entre a URL e a UI. Não é a primeira vez que vejo isso. Na maioria das SERPs que vi na minha carreira, essa era a prática.
Normalmente isso é feito para facilitar o manejo dos updates na UI, pelo mesmo princípio de usar um useState, por exemplo. Mas, neste cenário específico, é um design ruim, e explicarei o porquê.

Embora possa parecer uma boa ideia, ela funciona bem até os bugs começarem a aparecer.
A sincronização entre URL, estado e UI é nada menos que caótica, especialmente se você tem dezenas de filtros. É absolutamente propensa a erros, e muitos bugs vêm disso. Ao fazer manutenção ou adicionar uma nova funcionalidade, é preciso tocar em uma dúzia de lugares que compõem essa cola de sincronização entre as três partes do front-end.
Alguns bugs comuns que vi em mais de um projeto: uma gestão ruim de navegação de URL faz com que filtros sejam removidos da URL; navegar causa um loop de hooks useEffect que dispara e provoca loops infinitos ou renderizações desnecessárias; uma sincronização ruim ou incorreta pode deixar o estado desatualizado e fazer a UI representar coisas que não estão na URL.
E isso é só o começo da lista.
Como podemos fazer melhor?
Se a fonte dos bugs e da complexidade é a cola de sincronização, poderíamos ter uma funcionalidade equivalente sem ela?
Uma coisa que muitos desenvolvedores esquecem ao lidar com histórico/localização/roteador, seja React Router ou outro, é que o histórico é reativo como um state. Isso é tão verdade que usamos hooks para obter os search params de uma URL, por exemplo.
Então voltamos à primeira pergunta. Se o histórico é reativo, por que precisamos de uma cola de sincronização? Poderíamos usar a URL como estado para nossa UI e nos livrar de toda essa bagunça de sincronização?

Essa foi a pergunta que me encarreguei de responder esta semana, e os resultados são promissores.
Minha situação atual
Quando a página SERP carrega dados da API, um hook é responsável por atualizar a URL e navegar até a URL correspondente.
A API é disparada quando o context provider que mantém o estado muda. Dentro do provider há um useEffect que atualiza o estado com base na URL alterada.
O objeto retornado pelo context provider é uma representação do estado e algumas funções para alterá-lo.
A UI consome o estado do context provider.

Os vilões do filme são esses dois useEffects. São necessárias muitas verificações para impedir que esse loop se torne infinito. Isso é complexo, e os bugs se infiltram facilmente.
Vamos remover o estado intermediário e os useEffects que sincronizam tudo.
Removendo o estado intermediário
Faremos mudanças diretamente na URL usando as mesmas funções expostas pelo context provider. E usaremos as propriedades retornadas pelo context provider para retornar valores calculados a partir da URL, que alimentarão a chamada de API e a UI. A API dos componentes não mudará.

Este é um exemplo de pseudocódigo no provider:
const [searchParams, setSearchParams] = useSearchParams();
const [state, dispatch] = useReducer(reducer);
useEffect(() => {
dispatch({
action: 'SET_QUERY',
payload: getQueryFromUrl(searchParams)
});
}, [searchParams]);
return {
query: state.query,
setQuery: (query) =>
dispatch({
action: 'SET_QUERY',
payload: query
})
};
Lembre-se de que o código que constrói a URL está em outro lugar. Portanto, ele não é abordado neste provider.
O estado é completamente inútil.
const [searchParams, setSearchParams] = useSearchParams();
// const [state, dispatch] = useReducer(reducer);
// useEffect(() => {
// dispatch({
// action: 'SET_QUERY',
// payload: getQueryFromUrl(searchParams)
// });
// }, [searchParams]);
return {
query: getQueryFromUrl(searchParams),
setQuery: (query) => {
const newSearchParams = setQueryInUrl(query);
setSearchParams(newSearchParams);
}
};
Se você ler a linha useSearchParams como um useState, ficará mais fácil perceber o quanto o estado é inútil. Ambos os useEffects podem ser removidos. A lógica está em um só lugar. As mudanças nos filtros são previsíveis.
Isso pode ser feito para qualquer nível de complexidade e escala bem se você usar algumas boas práticas.
Resultados observados
A primeira coisa que se destaca é que aqueles bugs desagradáveis de query params sendo removidos da URL desapareceram completamente. Não há hooks useEffect executando em nenhum lugar que possam causar efeitos colaterais.
Além disso, ao chegar na página ou navegar até um produto e voltar, ela renderiza imediatamente exatamente o necessário, com os filtros aplicados. Antes, a página piscava enquanto useEffects decidiam o que estaria na UI e na URL. A fonte da verdade está na URL. Estou aproveitando diretamente uma API poderosa conectada à minha UI.
Escrever nos query params de uma URL não é tão fácil quanto atualizar uma propriedade de um objeto. Essa é a única parte que se torna complexa. Uma forma de facilitar é usar o objeto URLSearchParams. Ele funciona como um Map e torna muito mais fácil obter e definir query params. No fim, basta fazer searchParams.toString() e você terá sua query.
No geral, remover um monte de estado e useEffects malucos é uma excelente troca.
Conclusão
Acho essa solução elegante. Ela beneficia a manutenção e a experiência do usuário. Todos ganham.
O código removido era overengineering. Foi criado a partir de um conjunto de crenças e práticas que React, e Meta, nos fizeram confiar cegamente ao longo dos anos. A crença aqui é ter um estado obrigatório para os componentes de UI.
Um exemplo dessas crenças: quem já ouviu que o estado do Redux precisa ser serializável? No papel parece bonito: todos os princípios de imutabilidade e programação funcional, palavras sofisticadas e assim por diante.
Mas, no mundo real, quando você tem uma aplicação React rodando em Smart TVs, ter um objeto serializável no estado faz uma TV Samsung feita em 2015 carregar um catálogo de filmes em 1 minuto. Um minuto! A API não tem culpa, porque responde em 200ms. O processamento era o problema. A TV tinha dificuldades com objetos de estado do Redux na memória. Ir além dessas crenças e usar objetos não tão serializáveis nos fez melhorar o tempo de carregamento para 1,5 segundo. História real, da minha própria experiência.
Então, embora as práticas que aprendemos com o ecossistema React ao longo dos anos funcionem na maioria dos casos, ainda que de forma subótima, em cenários como o meu, seja em uma página SERP ou em uma Smart TV, elas simplesmente desmoronam e expõem a fragilidade da engenharia de software que cultivamos no desenvolvimento front-end.
Mantenham-se hidratados e se cuidem.