Reduce — JavaScript

Oct 16, 20216 minute read


Eu demorei bastante a entender como o reduce funciona, mais do que eu “me orgulho”. Mas tá tudo bem. Não é algo que você necessariamente precisa usar todos os dias. Embora algo aqui e acolá poderia ser simplificado com um uso do reduce, nos momentos em que nós não sabíamos disso provavelmente nos viramos de outra forma.

Foto por Brenda Godinez no Unsplash


Dependendo da sua bolha e redes sociais, provavelmente você já viu aquele clássico exemplo mostrando diversos emojis de fruta em um array e como o reduce de fato “reduz” o array a uma salada de frutas. É um exemplo bonitinho de ser ilustrado, mas não dá nenhuma ajuda real.

De forma curta, isso é uma implementação do reduce:

const arr = [1, 2, 3, 4, 5, 6];
const sum = arr.reduce((acc, current) => acc + current);

exemplo simples de reducer que soma os valores de um array

E pode ser outro exemplo dos que encontramos facilmente em todo lugar. De todo modo, vamos incrementando aos poucos!

O reduce, tal qual .map(), .filter(), .forEach() e outros, realizam uma iteração em um Array, com a diferença de que o reduce espera que a partir do seu array você gere um único valor.

No trecho de código acima, o que chamamos de acc ou accumulator (acumulador) é o valor anterior da iteração, que vai começar como o valor inicial do seu array1 (nesse exemplo, o 1). Enquanto isso, current é o valor atual da iteração, que nesse início vai ser o segundo elemento (o 2)2.

Podemos pensar então na execução desses passos da seguinte forma:

// 1 (valor inicial), 2 (valor atual) -> 1 + 2
// 3 (valor anterior), 3 (valor atual) -> 3 + 3
// 6 (valor anterior), 4 (valor atual) -> 6 + 4
// 10 (valor anterior), 5 (valor atual) -> 10 + 5
// 15 (valor anterior), 6 (valor atual) -> 15 + 6

console.log(sum); // 21

exemplo do que está acontecendo no código

const arr = [1];
cont result = arr.reduce((acc, current) => 2 * (acc + current));

exemplo simples de reducer que irá retornar o único valor do array

Mesmo que o retorno seja duas vezes a soma do acumulador e o valor atual, o retorno será 1. Isso porque ele nem estará sendo performado.

O .reduce() também aceita como segundo parâmetro um valor inicial. Nesse caso, invés do primeiro elemento ele irá começar desse seu valor inicial:

const arr = [1, 2, 3, 4, 5, 6];
const sum = arr.reduce((acc, current) => acc + current, 10);

console.log(sum); // 31

Caso você quisesse obter o mesmo resultado acima sem usar um reduce por exemplo, você teria algo assim:

const arr = [1, 2, 3, 4, 5, 6];

let sum = 10;
arr.forEach((value) => {
  sum += value;
});

console.log(sum); // -> 31

exemplo paralelo, usando forEach

# Reducer callback

Eu mencionei um pouco acima sobre a função ‘reducer’, que é a que você passa ao chamar .reduce(), mas ela vai além dos exemplos usados. Essa callback pode receber 4 parâmetros:

Os dois primeiros já foram explicados. Os dois últimos são ainda mais simples: o índice atual se refere (claro) ao índice do valor atual, isso significa que ele irá sempre começar em 1, que é o segundo elemento3 do seu array. Já o array, é simplesmente o array que “está sendo reduzido”. No exemplo inicial: [1, 2, 3, 4, 5, 6].

# Mas qual a relevância desses outros parâmetros?

Vamos supôr que você tenha um reducer complexo. Por organização, é bom mantê-lo separado para facilitar a manutenção. Neste caso, você precisa de uma forma de acessar o array recebido:

const reducer = (accumulator, current, currentIndex, array) => {
  const prefix = currentIndex === array.length - 1 ? 'e ' : ', ';
  return `${accumulator}${prefix}${current}`;
}

function getJoinedNames(names) {
  return names.reduce(reducer);
}

console.log(getJoinedNames(['zezinho', 'luizinho', 'iguinho')]); // 'zezinho, luizinho e iguinho'

exemplo simples de reducer que une um array de strings

Este exemplo é puramente didático, não deve ser tratado como a melhor forma de resolver esse tipo de problema.

Mas antes: o que esse trecho nos resolve?

Durante a iteração, foram usados os parâmetros currentIndex e array para descobrir se o elemento atual é o último, para assim definir se o prefixo será uma separação por vírgula ou por ‘e ‘. É importante que nós tenhamos acesso a essas informações mesmo em exemplos como esse, onde o reducer está fora do escopo da função e não sabe o que é o array original. É claro que nesse caso seria mais simples só manter o reducer no escopo da função, mas como mencionei, esse exemplo foi feito para fins didáticos.

# Caso de uso real

Recentemente, fiz a adição de uma funcionalidade que permite a criação de temas customizados para o nosso Design System no Gympass, o Yoga. Dentre as adições, houveram mudanças em código existente e uma das sugestões no review do meu pull request era a reescrita de um forEach para usar um reducer. Se você quiser ver o código, é só vir aqui: diff.

Pra evitar trazer toda a complexidade e contexto envolvidos, vou fazer um exemplo diferente aqui:

const components = [
  {
    name: "Button",
    styles: {
      color: "white",
      backgroundColor: "black",
      borderRadius: "20px",
    },
  },
  {
    name: "TextInput",
    styles: {
      color: "black",
      border: "2px solid",
    },
  },
  {
    name: "Checkbox",
    styles: {
      color: "deeppink",
    },
  },
];

const getComponentsMetadata = () =>
  components.reduce(
    (acc, current) => ({
      ...acc,
      [current.name]: {
        ...current.styles,
      },
    }),
    {},
  );

exemplo mínimo de como usamos o reduce em um projeto real

Resumindo: dado o array que temos no código, o retorno vai ser um objeto nesse formato:

{
  Button: {
    color: "white",
    borderRadius: "20px",
    backgroundColor: "black"
  },
  TextInput: { … },
  CheckBox: { … }
}

(resumido com { … }, a ideia é a mesma)

Antes isso era feito com um forEach, que tal como o reduce itera um array, mas não é feito para manipulações exatamente. O código original, que criava um objeto vazio que sofria mutações durante a iteração como:

components[field] = { ...components[index].styles }

Já com o reduce, temos apenas um novo objeto criado a partir de um array que não realizou mutações a nenhuma variável.

Há “controvérsias” e nem todo mundo acha que usar o reduce é um ganho. Mas ao menos, agora você tem essa ferramenta no seu canivete suíco.

Footnotes

  1. Caso você passe um valor inicial no seu reducer, esse comportamento é diferente, isso será mostrado mais abaixo.

  2. O seu array pode ter apenas um elemento, mas nesse caso, indiferentemente do retorno do seu reducer (a função que passamos dentro do reduce), o valor vai ser o único contido dentro do array original. Dado o seguinte trecho de código:

  3. Caso você não tenha passado um segundo parâmetro definindo o valor inicial.