Estamos lidando com cada vez mais dados, quando esses dados entram em nossa aplicação, temos que torná-los consumíveis para nossos usuários. Hoje, veremos como podemos criar filtros dinâmicos: você ativa ou desativa um determinado filtro e os dados são atualizados instantaneamente em sua tela.
Não se deixe enganar pela simplicidade da nossa aplicação de demonstração. Montaremos uma arquitetura robusta que pode ser utilizada para combinações de grandes quantidades de dados e filtros. Nosso código será flexível, dinâmico e sustentável.
Vou tentar guiá-lo a cada passo do caminho, explicando todos os prós e contras.
Vamos pular direto para ele!
Nossos dados
Usaremos um arquivo JSON que contém informações sobre os 60 filmes mais populares (atualmente) de acordo com o The Movie Database. É uma resposta personalizada de sua API que armazenamos dentro do arquivo movies.json
:
[
{
"title": "100% Wolf",
"year": 2020,
"votes": 65,
"genreIds": [
10751,
16,
14
],
"description": "Freddy Lupin, heir to a proud family line of werewolves..."
},
{
"title": "6 Underground",
"year": 2019,
"votes": 2869,
"genreIds": [
28,
53
],
"description": "After faking his death, a tech billionaire recruits..."
}
]
Como sempre, estamos usando uma estrutura de dados simples. Temos um arquivo adicional, genres.json
, que contém informações sobre os gêneros relevantes (um id
e um name
) com estrutura semelhante.
Quando renderizamos os filmes com um componente dedicado que contém um pouco de estilo, elas ficarão assim:
Como você pode ver, renderizamos um título, uma parte da descrição, o ano da data de lançamento (canto superior direito), o gênero, e o número de votos que o filme recebeu (o último não será usado neste artigo).
Antes de criarmos a lógica de filtragem real (código), permita-me dizer algumas palavras sobre filtragem de dados e expressões booleanas.
Filtrando dados
Ao renderizarmos nossa lista de Filmes, temos os dados para renderizar os filmes disponíveis como uma propriedade em nosso componente. É um array contendo um objeto de dados para cada filme. Então, quando falamos sobre filtrar nossos dados, estamos basicamente falando sobre como manipular esse array antes de usá-lo para renderizar os componentes de filme.
Você vê na captura de tela da nossa aplicação de demonstração que teremos dois grupos de filtros (em teoria, podem ser muitos mais):
- Filtros de ano
- Filtros de gênero
Você pode ficar tentado — e isso acontece com muita frequência — a armazenar dados relacionados a filtros em seu estado assim:
export default function Movies({ movies, genres }: MoviesProps) {
const yearsSelected = useState([2020, 2019]);
const genreIdsSelected = useState([14, 16, 10751]);
// data is filtered here
return (
<MovieList />
);
}
Por favor, não faça isso! Deixe-me explicar. Sempre que você vir um padrão repetitivo em seu código (nesse caso, o uso de useState para yearsSelected
e genreIdsSelected
são quase idênticos), você deve ficar alerta. Em quase todos esses casos, existe uma solução melhor e mais genérica.
Um exemplo de por que a configuração acima talvez não seja a melhor ideia: ela não contém nenhuma informação sobre como devemos filtrar os dados reais. Quais propriedades do nosso objeto Movie
são relevantes? O que fazemos com os valores (anos) de yearsSelected
? E onde estamos criando as funções que realmente filtrarão os dados? Como os dois grupos funcionais que temos (ano, gênero) se relacionam e trabalham juntos?
A pseudo-solução acima carece de estrutura e não está pronta para o futuro. E se precisarmos de mais grupos de filtros? E se precisarmos de 15 deles? Vamos fazer 15 ligações de useState
? Você não seria o primeiro a fazer isso, mas é um antipadrão. Isso causará problemas a longo prazo.
Vamos dar uma olhada em profundidade em uma solução abaixo. COmo mencionado antes, no entanto, uma palavra rápida sobre expressões booleanas. Na minha opinião, você precisa de uma compreensão muito sólida disso antes de poder fazer um plano para filtrar dados em sua própria aplicação.
Expressões booleanas
Expressões booleanas, ou expressões lógicas, usam operadores como OR
, AND
e NOT
. Seus equivalentes em JavaScript são ||
, &&
!
(ponto de exclamação), como você provavelmente sabe.
De volta aos nossos dois grupos de filtro: anos e gênero. Imagine que ativamos alguns filtros como esses:
Selecionamos dois anos e dois gêneros. O que isto significa? A resposta para essa pergunta não é tão direta quanto você imagina. Eu posso inventar vários.
Selecione filmes que:
- (lançados em 2018
OR
2017)AND
(pertencem ao gênero AnimationOR
Family).- (lançados em 2018
OR
2017)OR
(pertencem ao gênero AnimationOR
Family).- (lançados em 2018
OR
2017)AND
(pertencem ao gênero AnimationAND
Family).
Observe que as diferenças sutis nos operadores booelanos. Eles fazem toda a diferença. Como desenvolvedor, você deve fazer essas escolhas. Como funcionam os filtros? Você decide. Você provavelmente deseja discutir isso com o cliente ou o proprietário do produto e precisa garantir que o usuário final entenda como isso funciona quando estiver usando os filtros.
E por último, mas não menos importante, você e seus colegas precisam entender como seu próprio código funciona quando estiver pronto. É fácil se perder com isso, que é um motivo ainda maior para criar uma solução genérica que possa lidar com vários grupos de filtros.
Façamos a escolha: Devolvemos os filmes que foram lançados em algum dos anos selecionados e pertencem a todos os gêneros selecionados.
Opção 3 acima.
Hora de escrever algum código.
Filtros no Estado
O gerenciamento de estado é um assunto á parte. Sempre há várias soluções. Nesse caso, optamos por uma única chamada useState
para acompanhar os filtros selecionados:
enum Group {
YEAR = "year",
GENRE = "genre",
}
type Filter = {
name: string | number;
group: Group;
fnc: Function;
};
const [filters, setFilters] = useState<Filter[]>([]);
Ele contém um array de objetos do tipo Filter
. Cada filtro tem três pontos de dados (propriedades): um name
, um group
e uma function
.
Temos esses reducers para manipular nosso estado:
function filterExists(name: string | number, group: Group) {
return (
filters.find((f) => f.name === name && f.group === group) !== undefined
);
}
function addFilter(name: string | number, group: Group, fnc: Function) {
setFilters((currentFilters) => [...currentFilters, { name, group, fnc }]);
}
function removeFilter(name: string, group: Group) {
setFilters((currentFilters) =>
currentFilters.filter((f) => !(f.name === name && f.group === group))
);
}
function toggleFilter(name: string | number, group: Group, fnc: Function) {
if (filterExists(name, group)) {
removeFilter.apply(null, arguments);
} else {
addFilter.apply(null, arguments);
}
}
Quando clicamos em um dos “rótulos de filtro” (por falta de um termo melhor) em nossa interface, chamamos a função toggleFilter
. Podemos ver um exemplo — os rótulos para o grupo de filtros “anos”. O código relevante se parece com isso:
{getUniqueYears().map((year) => (
<LabelFilter
text={year}
active={filterExists(year, Group.YEAR)}
onClick={() =>
toggleFilter(year, Group.YEAR, (m: Movie) => m.year === year)
}
/>
))}
Veja como passamos não apenas um nome e um identificador de grupo, toggleFilter
, mas também uma função que pode ser usada para determinar se um filme deve ou não ser selecionado para esse filtro específico.
Fazemos algo semelhante para os rótulos de gênero:
<LabelFilter
text={genre.name}
active={filterExists(genre.name, Group.GENRE)}
onClick={() =>
toggleFilter(
genre.name,
Group.GENRE,
(m: Movie) => m.genreIds.indexOf(genre.id) !== -1
)
}
/>
Agora nosso estado tem muito conhecimento sobre como devemos filtrar nossos filmes. Vamos fazer exatamente isso a seguir.
Aplicando filtros
Em vez de apenas jogar o código no seu colo, vamos dar uma olhada nas partes individuais, uma a uma. É aqui que toda a mágica acontece, então vamos tentar ter certeza de que entendemos cada pedacinho dela.
A função principal onde aplicamos os filtros:
export function applyFilters(movies: Movie[], filters: Filter[]) {
return movies.filter((movie) => {
const showByYear = isShownByYear(movie, filters);
const showByGenre = isShownByGenre(movie, filters);
return showByYear && showByGenre;
});
}
A função acima recebe todos os movies
e os filters
em nosso estados. Bastante fácil. Em seguida, iteramos todos os filmes e, para cada filme, verificamos:
- Se for “mostrado por ano” (linha 3).
- Se for “mostrado por gênero” (linha 4).
"Espere um segundo! Isso é código idêntico novamente, não é? E dissemos anteriormente que deveríamos tentar evitar escrever códigos como esse.” Se esse foi seu primeiro pensamento, você está no caminho certo. No entanto, por uma questão de clareza, escrevemos explicitamente duas funções diferentes.
Na linha 5, retornamos o resultado para o filme específico. Se deve ser mostrado em nossa lista ou não:
return showByYear && showByGenre;
Lembre-se de nossa escolha para esta expressão lógica anterior:
(são lançados em 2018 OR 2017) AND (pertencem ao gênero Animation AND Family)
O operador &&
no trecho preto acima, representa o operador AND do meio na expressão completa.
Agora, vamos ver as funções auxiliares isShownByYear
e isShownByGenre
:
function isShownByYear(movie: Movie, filters: Filter[]) {
const yearFilters = filters.filter((filter) => filter.group === Group.YEAR);
if (!yearFilters.length) return true;
return yearFilters.some((filter) => filter.fnc(movie));
}
Para determinar se um filme deve ou não ser exibido com base nos anos atualmente selecionados, começamos selecionando apenas os filtros relevantes em nosso estado (linha 2). Se nenhum for encontrado, significa que nenhum ano foi selecionado e então fazemos a escolha que consideramos o filme selecionado (linha 3). É importante mencionar que também poderíamos ter optado por não selecionar o filme neste caso.
A última linha (linha 4) também é importante. Observe como usamos o método Array some
quando iteramos os filtros do ano. Se qualquer uma das chamadas retornar true
(por exemplo, se o filme foi lançado em algum dos anos atualmente selecionados), retornamos true
imediatamente. Isso representa o operador OR
em nossa expressão completa:
(são lançados em 2018 OR 2017) ...
Se quiséssemos usar um operador AND
, deveríamos ter usado every
. E é exatamente isso que fazemos dentro da outra função auxiliar isShownByGenre
:
function isShownByGenre(movie: Movie, filters: Filter[]) {
const genreFilters = filters.filter((filter) => filter.group === Group.GENRE);
if (!genreFilters.length) return true;
return genreFilters.every((filter) => filter.fnc(movie));
}
Veja como usamos every
em vez de some
aqui. Isso significa que o filme precisa pertencer a todos os gêneros selecionados para se qualificar. E foi exatamente isso que escolhemos na segunda parte da nossa expressão:
... (pertencem ao gênero Animation AND Family)
Conclusão
Quando se trata de expressões lógicas (booleanas), há muitas escolhas a serem feitas. Certifique-se de ter um plano inicial. Esclareça com as pessoas envolvidas o que você deseja alcançar antes de escrever o código real.
A solução apresentada neste artigo pode não ser perfeita, mas demonstra como podemos normalizar a lógica e os problemas em soluções genéricas escaláveis e testáveis.
Não mencionei memoização ou outras técnicas de cache. Isso foi de propósito, pois só iria distrair do assunto. Com isso dito, você deve considerar otimizar ainda mais o código apresentado — especialmente se seu conjunto de dados for muito maior do que o que vimos hoje.
JavaScript é muito poderoso. Com as técnicas e otimizações corretas, ele pode lidar com grandes quantidades de dados e iterações.
Obrigado pelo seu tempo!
Top comments (0)