Array Methods: map, filter, reduce

The transformations you will reach for in every list you render.

Rendering a list means turning an array of data into an array of elements. These three methods do that work, and each one hands you something new instead of changing the array you gave it.

map

map runs your function once per item and collects the results into a new array.

const shows = [
  { title: "Severance", rating: { average: 8.7 } },
  { title: "Andor", rating: { average: 8.4 } },
  { title: "Kaos", rating: { average: 7.2 } },
];

const titles = shows.map((show) => show.title);

// Braces make a function body, so this arrow returns nothing.
const noReturn = shows.map((show) => { show.title });

console.log("titles:  ", titles);
console.log("noReturn:", noReturn);
console.log("first item is still:", shows[0]);

titles is a new array of three strings. noReturn is three undefined values, because that arrow has a body and no return. The last line shows shows[0] is still the object it always was.

map vs forEach

forEach also runs your function once per item, but it throws the results away and returns undefined.

const shows = [
  { title: "Severance", rating: { average: 8.7 } },
  { title: "Andor", rating: { average: 8.4 } },
];

const fromForEach = shows.forEach((show) => show.title);
const fromMap = shows.map((show) => show.title);

console.log("forEach returns:", fromForEach);
console.log("map returns:    ", fromMap);

The first line reads undefined. The second reads the two titles. Use forEach to do something with each item, and map when you want a result.

filter

filter keeps the items your function says yes to. Your function has to answer with a condition, not a value.

const shows = [
  { title: "Severance", rating: { average: 8.7 } },
  { title: "Andor", rating: { average: 8.4 } },
  { title: "Shogun", rating: { average: 8.9 } },
  { title: "Kaos", rating: { average: 7.2 } },
];

const good = shows.filter((show) => show.rating.average >= 8);

// rating is an object, and every object is truthy, so nothing is dropped.
const byRatingAlone = shows.filter((show) => show.rating);

console.log("all shows:", shows.length);
console.log("good ones:", good.length);
console.log("kept:     ", good.map((show) => show.title));
console.log("rating alone kept:", byRatingAlone.length);

Three of the four shows are rated 8 or above. The last line keeps all four, because show.rating is an object and every object is truthy. Compare the number you mean to compare.

find

find, from Working with Arrays, takes the same kind of function as filter. It hands back the first match on its own, not a list.

const shows = [
  { title: "Severance", genres: ["Drama", "Thriller"] },
  { title: "Kaos", genres: ["Comedy", "Fantasy"] },
  { title: "Shogun", genres: ["Drama", "History"] },
];

const drama = shows.find((show) => show.genres.includes("Drama"));
const dramas = shows.filter((show) => show.genres.includes("Drama"));

console.log("find:        ", drama.title);
console.log("filter:      ", dramas.map((show) => show.title));
console.log("filter title:", dramas.title);

find gives one show, so drama.title works. filter gives an array of two, so dramas.title is undefined — an array has no title. Use find for one show and filter for a list.

reduce

reduce turns a whole array into one value. Your function receives the value so far and the current item, and returns the next value so far. The second argument to reduce is where that value starts.

const shows = [
  { title: "Severance", rating: { average: 8.7 } },
  { title: "Andor", rating: { average: 8.4 } },
  { title: "Kaos", rating: { average: 7.2 } },
];

const total = shows.reduce((sum, show) => sum + show.rating.average, 0);

// Without the 0, the first show object becomes the starting sum.
const noStart = shows.reduce((sum, show) => sum + show.rating.average);

console.log("total:   ", total);
console.log("average: ", (total / shows.length).toFixed(1));
console.log("no start:", noStart);

The total is 24.3 and the average reads 8.1. The last line reads [object Object]8.47.2: with no starting value, the first show became the sum, and adding a number to an object gives a string. Always pass the starting value. It also stops reduce throwing on an empty array.

Chaining

Each method returns a new array, so you can hang the next one off it. Filter first, then map: there is no point transforming items you are about to drop.

const shows = [
  { title: "Severance", rating: { average: 8.7 } },
  { title: "Andor", rating: { average: 8.4 } },
  { title: "Shogun", rating: { average: 8.9 } },
  { title: "Kaos", rating: { average: 7.2 } },
];

const goodTitles = shows
  .filter((show) => show.rating.average >= 8)
  .map((show) => show.title);

// Mapping first throws the rating away, so the filter has nothing to read.
const wrongOrder = shows
  .map((show) => show.title)
  .filter((title) => title.rating >= 8);

console.log("filter then map:", goodTitles);
console.log("map then filter:", wrongOrder);

The first chain gives three titles. The second gives an empty array: after map, each item is a string, and title.rating on a string is undefined.

Methods that mutate

sort is not like map. It rearranges the array you gave it.

const titles = ["Severance", "Andor", "Kaos"];

titles.sort();

// sort changed this array; it did not hand back a new one.
console.log("titles after sort:", titles);

const more = ["Severance", "Andor", "Kaos"];

console.log("toSorted gives:   ", more.toSorted());
console.log("more is still:    ", more);

titles comes out in a new order even though nothing was assigned to it. toSorted does the same job and leaves more alone.

push, pop, splice, sort, and reverse all change the array in place. map, filter, slice, and toSorted all hand back a new one. toSorted is recent, so older projects copy first with [...titles].sort(), which Destructuring, Spread, and Rest covers. Both still compare items as text, which Working with Arrays shows.

Common mistakes

  • Using forEach when you want a result. It always returns undefined; use map.
  • Braces in an arrow callback. (show) => { show.title } returns nothing, so map gives you undefined for every item.
  • Sorting an array you did not copy. sort changes it in place; use toSorted or copy first.

Exercise

This should log the title of every streaming show rated above 8, one per line. Right now it logs each whole show object instead.

The output should read:

Severance
Shogun

Filter first, then map. Remember rating is an object.

const shows = [
  { title: "Severance", streaming: true, rating: { average: 8.7 } },
  { title: "Andor", streaming: false, rating: { average: 8.4 } },
  { title: "Shogun", streaming: true, rating: { average: 8.9 } },
  { title: "Kaos", streaming: true, rating: { average: 7.2 } },
];

// Your turn: filter, then map.
const goodTitles = shows;

goodTitles.forEach((title) => console.log(title));