Filtragem de dados avançada em React
Tags: react, typescript, tutorial, braziliandevs
Published: 2023-01-18 18:19:52

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.


![Imagem de um filtro](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y83s0ghqduevx85xxgdf.png)

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`:

```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:


![Imagem de filter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lykt8lfdaqc6u0rjqzig.png)

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

![Imagem de filtro](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wb2f85aiqojkndw7rcxy.png)

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:

```jsx
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:


![Image de filtro](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ec62ayzobln7p0p2zx79.png)

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:

> 1. (lançados em 2018 `OR` 2017) `AND` (pertencem ao gênero Animation `OR` Family).
> 2. (lançados em 2018 `OR` 2017) `OR` (pertencem ao gênero Animation `OR` Family).
> 3. (lançados em 2018 `OR` 2017) `AND` (pertencem ao gênero Animation `AND` 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:

```jsx
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:

```jsx
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:

```jsx
{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:

```jsx
<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:

```jsx
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:

```jsx
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`:

```jsx
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`:

```jsx
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!