Pregunta 1¿Qué recibe como primer argumento el callback que le pasas a reduce?
reduce — reducir un array a un solo valor
Aprende reduce, que combina un array en un solo valor. Verás el acumulador y el valor inicial, sumas, máximos, agrupación y el TypeError con arrays vacíos.
Si necesitas el total de las líneas de un pedido, map mantiene el número de elementos y filter solo quita algunos, así que en ambos casos lo que obtienes es un array. Ninguno de los dos sirve para combinar un array en un solo valor, como un total o un máximo.
En este artículo verás reduce, que combina un array en un solo valor, y el valor inicial desde el que empieza.
Combinar un array en un solo valor — reduce y el valor inicial
Supón que quieres mostrar en pantalla el total de un carrito de compras. Con forEach, la variable que guarda el total tiene que quedar fuera del bucle, donde cualquier línea posterior podría sobrescribirla. Y como su valor cambia con cada suma, tienes que declararla con let en lugar de const.
El método de array reduce (un método que pasa los elementos a un callback uno por uno y devuelve solo el último valor de retorno) recibe un callback cuyo primer parámetro es el acumulador (el parámetro que guarda el valor devuelto en la llamada anterior). A diferencia de map y filter, el elemento va en segundo lugar. En el segundo argumento del propio reduce escribes el valor inicial (el valor que tiene el acumulador en la primera llamada).
const cartItems = [
{ name: "Soporte para laptop", price: 3200 },
{ name: "Hub USB", price: 2480 },
{ name: "Cable HDMI", price: 1180 },
];
// El primer parámetro es el valor de retorno de la llamada anterior; el segundo, el elemento actual
const addPrice = (total, item) => total + item.price;
// El segundo argumento, 0, es el total en la primera llamada
const totalPrice = cartItems.reduce(addPrice, 0);
console.log(totalPrice); // 6860
// Lo que se devuelve es un solo valor, no un array
console.log(typeof totalPrice); // number
total a la llamada 2. El 6860 que devuelve la última llamada es el resultado de reduce.No necesitas una variable propia para el total parcial, porque reduce se encarga de pasar el valor de una llamada a la siguiente. Y lo que se pasa no tiene que ser un número: también puede ser un elemento o un objeto.
Quedarse con el mayor — pasar un número o un elemento
Supón que quieres saber cuál de los videos que subiste tiene más reproducciones. Esta vez no sumas nada: comparas valores y solo pasas el mayor a la siguiente llamada. La pregunta es con qué valor debe empezar el acumulador.
El acumulador puede guardar un número o el propio elemento. Si pasas 0 como segundo argumento, lo que va de una llamada a otra es un número; si pasas videos[0], es el propio elemento. Usa como valor inicial el mismo tipo de valor que devuelve el callback.
const videos = [
{ title: "SQL básico #1", views: 8200 },
{ title: "Linux básico #3", views: 15400 },
{ title: "Fundamentos de Git", views: 9700 },
];
// El valor inicial es un número, así que compara y devuelve números
const maxViews = videos.reduce((max, video) => Math.max(max, video.views), 0);
console.log(maxViews); // 15400
// El valor inicial es un elemento, así que compara y devuelve elementos
const topVideo = videos.reduce(
(top, video) => (video.views > top.views ? video : top),
videos[0],
);
console.log(topVideo.title); // Linux básico #3
Las dos llamadas a reduce comparan views; lo único que cambia es lo que se pasa a la siguiente llamada. Si a la segunda le dieras 0 como valor inicial, top.views sería undefined en la primera llamada, así que la comparación daría false cada vez y se devolvería 0 hasta el final.
Un valor inicial de 0 falla con números negativos
En un array que puede tener valores negativos, como un registro de temperaturas, si empiezas desde 0, el 0 sigue siendo el mayor aunque el máximo real sea -3, y el resultado es un valor que no está en el array. Si comparas los elementos entre sí y empiezas desde el primer elemento del array, el rango de los valores deja de importar.
Reducir conteos a un objeto — agrupación
Supón que quieres saber cuántas consultas de soporte hay en cada categoría. A diferencia de un total, el resultado no es un solo número: necesitas un valor por categoría. Podrías crear un objeto vacío fuera del bucle e ir sumando con forEach, pero entonces el objeto del recuento vuelve a quedar fuera del bucle, donde cualquier línea podría sobrescribirlo.
También puedes usar {} como valor inicial. En ese caso, el acumulador es el objeto donde se lleva el recuento, y el callback actualiza una clave y luego devuelve ese objeto. Leer una clave que todavía no existe da undefined, así que añade ?? 0 para empezar a contar desde 0.
const tickets = [
{ subject: "Quiero devolver un producto", category: "Devoluciones" },
{ subject: "Mi pedido no ha llegado", category: "Envíos" },
{ subject: "Necesito otra etiqueta de envío", category: "Envíos" },
];
// Suma 1 al conteo de la categoría de cada consulta
const countByCategory = tickets.reduce((counts, ticket) => {
counts[ticket.category] = (counts[ticket.category] ?? 0) + 1;
return counts; // Pasa el counts actualizado a la siguiente llamada
}, {}); // Este {} es counts en la primera llamada
console.log(JSON.stringify(countByCategory)); // {"Devoluciones":1,"Envíos":2}
console.log(countByCategory["Envíos"]); // 2
counts que recibe y devuelve ese mismo objeto. El único objeto en el que se escribe es el que se pasó como valor inicial.El {} inicial es un objeto nuevo que solo guarda el recuento, así que no se escribe nada en los elementos de tickets. La tabla de abajo resume el valor inicial y lo que devuelve el callback en los 3 usos que has visto hasta ahora.
| A qué quieres reducir | Valor inicial | Lo que devuelve el callback |
|---|---|---|
| Un total | 0 | El total anterior más el valor |
| El elemento mayor | El primer elemento del array | El elemento que resulte mayor |
| Conteos por categoría | Un objeto vacío | El objeto con la clave actualizada |
Cuando no hay elementos — reduce sin valor inicial
Un resultado filtrado puede quedar vacío. Si calculas el total de los envíos de un día en el que no hubo ninguno, reduce devuelve 0 o lanza un error, según cómo lo hayas escrito.
Puedes omitir el segundo argumento, el valor inicial. Si lo omites, el primer elemento del array pasa a ser el acumulador inicial, y la primera llamada al callback recibe el elemento del índice 1. Un array vacío no tiene primer elemento que tomar, así que se lanza un TypeError.
const yesterdayAmounts = [3200, 2480, 1180];
const todayAmounts = [];
// Sin valor inicial, el primer elemento, 3200, es el total inicial
console.log(yesterdayAmounts.reduce((total, amount) => total + amount)); // 6860
// Incluso con un array vacío, si hay valor inicial, se devuelve tal cual
console.log(todayAmounts.reduce((total, amount) => total + amount, 0)); // 0
// Sin valor inicial, no hay forma de decidir el total inicial
// todayAmounts.reduce((total, amount) => total + amount);
// TypeError: Reduce of empty array with no initial value
Este tipo de error es fácil de pasar por alto al escribir el código: funciona con los datos que tienes y solo falla el primer día en que ningún elemento cumple la condición. Cuando pases a reduce un resultado filtrado, pásale un valor inicial por si queda vacío.
Si omites {}, se escribe en el primer elemento
Si omites el {} final en el ejemplo de countByCategory, el propio objeto de consulta tickets[0] pasa a ser el counts inicial. No se lanza ningún error: a ese objeto se le agrega una clave Envíos, y la consulta de Devoluciones nunca se cuenta. Aunque el array no esté vacío, pasa siempre un valor inicial cuando reduzcas a un objeto.
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Si pasas un valor inicial de 0 a un reduce que compara los views de los elementos y devuelve el mayor, ¿qué se obtiene?
Pregunta 3¿Qué pasa si llamas a reduce sin valor inicial sobre un array vacío?