DEV Community

Paolo Enrico Iacono Fullone
Paolo Enrico Iacono Fullone

Posted on

Higher Order Function Reduce com Objetos

Este post é uma tradução autorizada do artigo escrito por Tony Wallace disponível em inglês no RedBit e no Dev.to.

Thanks Tony!

Introdução
Vamos olhar rapidamente como o Array.reduce funciona. Se você já está familiarizado com o funcionamento básico pule esta parte.
O Array.reduce reduz um array a um valor único. O valor resultante pode ser de qualquer tipo e não precisa ser necessariamente um array. Essa é uma das formas em que o array.reduce é diferente dos outros métodos como 'map' e 'filter'. Abaixo um exemplo de como o reduce retorna a soma de um array de números.

Exemplo 1:

const numeros = [1, 2, 3, 4, 5];
const soma = numeros.reduce((proximo, numero) => {
  return proximo + numero;
}, 0);
Enter fullscreen mode Exit fullscreen mode

A reduce aceita dois argumentos:

  1. Uma callback function que é executada para cada item no array e recebe os seguintes parâmetros:
  • O acumulador ('proximo' no exemplo acima), que é o valor que vamos trabalhar, na primeira iteração ele recebe o valor inicial 0. Para todas as iterações seguintes o acumulador é o valor retornado da iteração anterior;
  • O item atual do array ('numero' no exemplo acima);
  • O índice ('index' no exemplo acima) que não foi utilizado;
  • O array que está sendo trabalhado (não usado no exemplo acima);
  • O valor inicial do acumulador no exemplo acima foi definido como 0.
  1. A expressão do "Exemplo 1" vai executar uma callback function cinco vezes com os seguintes valores:

  2. Acumulador(proximo): 0 (o valor inicial); Valor (numero): 1; Retorno: 1;

  3. Acumulador: 1; Valor: 2; Retorno: 3.

  4. Acumulador: 3; Valor: 3; Retorno: 6.

  5. Acumulador: 6; Valor: 4; Retorno: 10.

  6. Acumulador: 10; Valor: 5; Retorno: 15.
    O valor final da 'soma' será 15.

Array.reduce aplicado a objetos

Lembrando que a reduce pode conter valores iniciais e finais de qualquer tipo, o que a torna muito flexível. Vamos explorar como podemos usar a reduce para tarefas comuns com objetos de uma dimesão.

1. Convertendo um array de objetos para um único objeto usando seu id como referência.

Desenvolvedores frequentemente tem que procurar um valor em um array usando um valor de outro array. Considerando o exemplo abaixo onde temos um array de objetos representando usuários e outro array representando seus perfis. Cada usuário tem uma propriedade 'id' e cada perfil tem uma propriedade 'userId'. Precisamos ligar cada usuário ao seu perfil, onde 'user.id' seja igual a 'profile.userId'. Uma implementação básica é mostrada no Exemplo 2.

Exemplo 2:
Deste exemplo em diante, não iremos traduzir o código ok?

const users = [
  { id: 1, email: 'sgiannattasio@email.tld' },
  { id: 2, email: 'tcarneiro@email.tld' },
  { id: 3, email: 'mflash@email.tld' },
];

const profiles = [
  { userId: 1, firstName: 'Silvinha', lastName: 'Giannattasio' },
  { userId: 2, firstName: 'Thalles', lastName: 'Carneiro' },
  { userId: 3, firstName: 'Murilo', lastName: 'The Flash' },
];

const usersWithProfiles = users.map((user) => {
  const profile = profiles.find((profile) => (user.id === profile.userId));
  return { ...user, profile };
});

// usersWithProfiles:
[
  {id: 1, email: 'sgiannattasio@email.tld', profile: { userId: 1, firstName: 'Silvinha', lastName: 'Giannattasio' }},
  {id: 2, email: 'tcarneiro@email.tld', profile: { userId: 2, firstName: 'Thalles', lastName: 'Carneiro' }},
  {id: 3, email: 'mflash@email.tld', profile: { userId: 3, firstName: 'Murilo', lastName: 'The Flash' }}
]
Enter fullscreen mode Exit fullscreen mode

O problema com o Exemplo 2 é que ele utiliza array.find dentro de array.map, o que é ineficiente. Este pode não ser um problema em pequenos arrays como os utilizados neste exemplo, mas caso esta solução seja aplicada em arrays maiores, maior será o tempo de procura de um perfil. Nós podemos resolver este problema transformando o array 'profiles' em um objeto utilizando a propriedade 'userId' como chave:

Exemplo 3:

const users = [
  { id: 1, email: 'carolzita@email.tld' },
  { id: 2, email: 'baeta@email.tld' },
  { id: 3, email: 'cadu@email.tld' },
];

const profiles = [
  { userId: 1, firstName: 'Caról', lastName: 'Silva' },
  { userId: 2, firstName: 'Henrique', lastName: 'Baeta' },
  { userId: 3, firstName: 'Carlos', lastName: 'Patricio' },
];

// Transformando os perfis em um objeto indexado pelo campo userId:
const profilesByUserId = profiles.reduce((next, profile) => {
  const { userId } = profile;
  return { ...next, [userId]: profile };
}, {});

// profilesByUserId:
// {
//   1: { userId: 1, firstName: 'Caról', lastName: 'Silva' },
//   2: { userId: 2, firstName: 'Henrique', lastName: 'Baeta' },
//   3: { userId: 3, firstName: 'Carlos', lastName: 'Patricio' },
// }

// Pesquisando os perfis pelo id:
const usersWithProfiles = users.map((user) => {
  return { ...user, profile: profilesByUserId[user.id] };
});

// usersWithProfiles:
// [
//   { id: 1, email: 'carolzita@email.tld', profile: { userId: 1, firstName: 'Caról', lastName: 'Silva' } },
//   { id: 2, email: 'hbaeta@email.tld', profile: { userId: 2, firstName: 'Henrique', lastName: 'Baeta' } },
//   { id: 3, email: 'cadu@email.tld', profile: { userId: 3, firstName: 'Carlos', lastName: 'Patricio' } },
// ]

console.log(usersWithProfiles);
Enter fullscreen mode Exit fullscreen mode

O Exemplo 3 gera o mesmo resultado do Exemplo 2, mas será muito mais rápido com arrays longas.

  1. Copiando um objeto com propriedades filtradas: Algumas vezes precisamos copiar um objeto com apenas algumas propriedades do objeto original, ou seja, omitindo algumas propriedades. Este é um excelente uso para o Array.reduce.

Exemplo 4:

// Copiando um objeto, mantendo as propriedades permitidas:
const person = {
  firstName: 'Orpheus',
  lastName: 'De Jong',
  phone: '+1 123-456-7890',
  email: 'fake@email.tld',
};

const allowedProperties = ['firstName', 'lastName'];

const allKeys = Object.keys(person);
const result = allKeys.reduce((next, key) => {
  if (allowedProperties.includes(key)) {
    return { ...next, [key]: person[key] };
  } else {
    return next;
  }
}, {});

// resultado:
// { firstName: 'Orpheus', lastName: 'De Jong' }
Enter fullscreen mode Exit fullscreen mode

No Exemplo 4 usamos a reduce para obter um resultado onde somente as propriedades incluídas na array "allowedProperties" sejam copiadas para um novo array, isso significa que podemos adicionar novas propriedades no objeto person, sem que essas propriedades sejam acessadas pelo array resultante da reduce.

Exemplo 5:

// Copiando um objeto, excluindo as propriedades não permitidas:

const person = {
  firstName: 'Orpheus',
  lastName: 'De Jong',
  phone: '+1 123-456-7890',
  email: 'odj@email.tld',
};

const disallowedProperties = ['phone', 'email'];

const allKeys = Object.keys(person);
const result = allKeys.reduce((next, key) => {
  if (!disallowedProperties.includes(key)) {
    return { ...next, [key]: person[key] };
  } else {
    return next;
  }
}, {});

// resultado:
// { firstName: 'Orpheus', lastName: 'De Jong' }
Enter fullscreen mode Exit fullscreen mode

No Exemplo 5 fizemos o contrário, o novo objeto vai conter todas as chaves do objeto 'person' que não estão incluídas no array 'disallowedProperties'. Caso alguma nova propriedade seja adicionada no objeto 'person', essa propriedade vai aparecer no resultado, a não ser que esta nova propriedade também seja inserida no array 'disallowedProperties'. Se você quer ter certeza que somente algumas propriedades serão incluídas no resultado, o Exemplo 4 é a melhor escolha, mas o Exemplo 5 é útil quando precisamos que somente algumas propriedades nunca sejam incluídas em um novo array.
Também podemos criar funções genéricas para os exemplos 4 e 5:

const filterAllowedObjectProperties = (obj, allowedProperties = []) => {
  return Object.keys(obj).reduce((next, key) => {
    if (allowedProperties.includes(key)) {
      return { ...next, [key]: obj[key] };
    } else {
      return next;
    }
  }, {});
}

const filterDisallowedObjectProperties = (obj, disallowedProperties = []) => {
  return Object.keys(obj).reduce((next, key) => {
    if (!disallowedProperties.includes(key)) {
      return { ...next, [key]: obj[key] };
    } else {
      return next;
    }
  }, {});
}
Enter fullscreen mode Exit fullscreen mode

Mesclando dois objetos, mantendo os valores de um deles.

Outra tarefa bastante comum é mesclar objetos com outros objetos que contenham valores padronizados para algumas propriedades. Algumas vezes podemos fazer isso usando o spread operator para "espalhar" os itens, mas podemos ter consequências inesperadas quando temos propriedades nulas ou vazias:

Exemplo 7:

const obj1 = {
  key1: 'value 1.1',
  key2: null,
  key3: 'value 1.3',
  key4: ''
};

const obj2 = {
  key1: 'value 2.1',
  key2: 'value 2.2',
  key3: 'value 2.3',
  key4: 'value 2.4',
  key5: 'value 2.5'
};

const result = { ...obj2, ...obj1 };

// result:
//  {
//    key1: 'value 2.1',
//    key2: null,
//    key3: 'value 2.3',
//    key4: '',
//    key5: 'value 2.5'
//  };
Enter fullscreen mode Exit fullscreen mode

O Exemplo 7 cria um novo objeto contendo as propriedades de 'obj2' sobrescritas pelas propriedades de 'obj1'. Note que o resultado mantém os valores nulos e uma string vazia do 'obj1'. Este comportamento acontece pois 'null' e uma string vazia são valores definidos no JavaScript. Provavelmente não queriamos esse resultado, mas o 'array.reduce' nos traz uma solução para este problema.

Exemplo 8:

const obj1 = {
  key1: 'value 1.1',
  key2: null,
  key3: 'value 1.3',
  key4: ''
};

const obj2 = {
  key1: 'value 2.1',
  key2: 'value 2.2',
  key3: 'value 2.3',
  key4: 'value 2.4',
  key5: 'value 2.5'
};

// Espalhando as propriedades dos dois objetos em um array.
const allKeys = [ ...Object.keys(obj1), ...Object.keys(obj2) ];

// Convertendo o array de propriedades em um set para remover os valores duplicados,
// e espalhando os valores únicos em um novo array.
const uniqueKeys = [ ...new Set(allKeys) ];

// Reduzindo as propriedades únicas em um novo objeto contendo o  // valor de cada chave do obj1, revertendo para o valor do obj2   // caso o obj1[key] seja um falsey.
const result = uniqueKeys.reduce((next, key) => {
  const value = obj1[key] || obj2[key];
  return { ...next, [key]: value };
}, {});

// resultado:
// {
//   key1: 'value 1.1',
//   key2: 'value 2.2',
//   key3: 'value 1.3',
//   key4: 'value 2.4',
//   key5: 'value 2.5',
// }
Enter fullscreen mode Exit fullscreen mode

Observe que o Exemplo 8 usa uma estratégia ingênua para decidir quando optar por utilizar o valor de ('obj2[key]') quando o valor padrão de ('obj1[key]') for falsey. Falsey em JavaScript são os valores não definidos (undefined), nulos (null), string vazia, '0' ou falso (false). Esta estratégia pode não ser apropriada para os casos em que estes valores sejam aceitáveis. Revise a condição de decisão de valores padrões conforme necessário. For exemplo substituindo 'const value = obj1[key] || obj2[key];' por 'const value = (obj1[key] !== undefined && obj1[key] !== null) ? obj1[key] : obj2[key];' vai garantir que o valor substituto só será usando quando o valor padrão for 'undefined' ou 'null'.

Analisando textos de pesquisa / consulta:

Por fim vamos analisar uma tarefa bastante comum que desenvolvedores costumam usar bibliotecas para realizar: Análise de textos de busca. Navegadores modernos fornecem URLSearchParams() que resolvem rapidamente este problema, mas talvez você não esteja escrevendo este código para um navegador, ou você tenha que dar suporte ao Internet Explorer ou só quer tentar de uma forma diferente pois é assim que aprendemos. Qualquer que seja a razão, a array.reduce pode nos ajudar.

Primeiro, preciamos de um texto de pesquisa, podemos obter diretamente de 'window.location.search' em um navegador ou pela URL, se você usa o React e react-router você pode usar o 'useLocation' hook:

`const { search = '' } = useLocation();`
Enter fullscreen mode Exit fullscreen mode

Entratanto, se você recebe uma string de pesquisa, é necessário prepará-la antes.

Exemplo 9a:

// Obtendo o texto da pesquisa:
const search = '?key1=value%201&key2=value%202&key3=value%203';

// Removendo o '?':
const query = search.replace(/^\?/, '');

// Separando a string no & comercial para criar um novo array de propriedades e valores:
const pairs = query.split('&');

// pares:
// [ 'key1=value%201', 'key2=value%202', 'key3=value%203' ];
Enter fullscreen mode Exit fullscreen mode

Em seguida, vamos transformar a chave-valor em um objeto dividindo pelo sinal de igualdade. A string antes do = é a chave e o restante é o valor. O valor precisa ser decodificado com o decoreURIComponent:

Exemplo 9b:

const params = pairs.reduce((next, pair) => {
  const [ key, value ] = pair.split('=');
  const decodedValue = decodeURIComponent(value);
  return { ...next, [key]: decodedValue };
}, {});

// params:
// {
//   key1: 'value 1',
//   key2: 'value 2',
//   key3: 'value 3',
// }
Enter fullscreen mode Exit fullscreen mode

O "parser" no Exemplo 9a/9b vai resolver em muitos casos, mas está incompleto. Strings de busca podem conter multiplos valures para cada chave, e este "parser" vai reter somente o último valor de cada chave, vamos consertar isso:

Exemplo 10:

const search = '?key1=value%201&key2=value%202&key3=value%203.1&key3=value%203.2&key3=value%203.3';
const query = search.replace(/^\?/, '');
const pairs = query.split('&');

const params = pairs.reduce((next, pair) => {
  const [ key, value ] = pair.split('=');
  const decodedValue = decodeURIComponent(value);
  const previousValue = next[key];
  let nextValue;

  if (previousValue !== undefined) {
    if (Array.isArray(previousValue)) {
      nextValue = [ ...previousValue, decodedValue ];
    } else {
      nextValue = [ previousValue, decodedValue ];
    }
  } else {
    nextValue = decodedValue;
  }

  return { ...next, [key]: nextValue };
}, {});

// params:
// {
//   key1: 'value 1',
//   key2: 'value 2',
//   key3: [ 'value 3.1', 'value 3.2', 'value 3.3' ],
// }
Enter fullscreen mode Exit fullscreen mode

O Exemplo 10 prepara a string exatamente como no Exemplo 9a. A diferença é como a callback da reduce lida com o valor para cada chave. Vejamos um passo a passo da callback function:

  1. O par chave-valor é dividido pelo sinal de igualdade = para separar o texto da chave do texto do valor.
  2. O valor é decodificado com o decodeURIComponent.
  3. O acumulador (next) é verificado para determinar se existe um valor prévio para a chave.
  4. Se existir um valor prévio (previousValue !== undefined) é feita outra verificação para determinar se ele é um array.
  5. Se o valor prévio é uma array, o valor decodificado é inserido nela. (nextValue = [ ...previousValue, decodedValue ];) Se o valor prévio não for uma array, uma nova array é criada contendo o valor prévio e o valor decodificado. (nextValue = [ previousValue, decodedValue ];)
  6. Se não existir valor prévio, o próximo valor é definido como valor decodificado. (nextValue = decodedValue;)

Os objeto 'params' resultante contém a string para key1 e key2, e uma array contendo as três strings para a key3 na ordem que apareceram na string de busca.

Como fizemos no Exemplo 1, podemos esclarecer o processo fazendo uma análise passo a passo de cada iteração.

  1. Accumulator (next): {} (o valor inicial); Valor (pair): 'key1=value%201; Returns: { key1: 'value 1' };
  2. Accumulator: { key1: 'value 1' }; Valor: 'key2=value%202; Returns: { key1: 'value 1', key2: 'value 2' };
  3. Accumulator: { key1: 'value 1', key2: 'value 2' }; Valor: 'key3=value%203.1; Returns: { key1: 'value 1', key2: 'value 2', key3: 'value 3.1' };
  4. Accumulator: { key1: 'value 1', key2: 'value 2', key3: 'value 3.1' }; Valor: 'key3=value%203.2; Returns: { key1: 'value 1', key2: 'value 2', key3: ['value 3.1', 'value 3.2'] };
  5. Accumulator: { key1: 'value 1', key2: 'value 2', key3: ['value 3.1', 'value 3.2'] }; Valor: 'key3=value%203.3; Returns: { key1: 'value 1', key2: 'value 2', key3: ['value 3.1', 'value 3.2', 'value 3.3'] };

Resumo: Array.reduce é um canivete suíço que podemos usar para resolver uma gama de problemas. Eu encorajo que que você explore a reduce e tente aplicar em muitas situações que talvez você não tenha considerado.

Top comments (0)