What to do with lists of things in JavaScript
Tags: functional, javascript, fundamentals
Published: 2018-09-23 20:02:44

---
title: What to do with lists of things in JavaScript
published: true
description: A quick guide to effectively searching and transforming lists of values with a functional twist
tags: functional, javascript, fundamentals
cover_image: https://thepracticaldev.s3.amazonaws.com/i/lsrbsiv07f62v2bapdcj.jpg
series: Lists of things
---

_Cover image by [Internet Archive Book Image](https://www.flickr.com/photos/internetarchivebookimages/14587278329/)_

In JavaScript, as in most languages, we have a data structure that deals with lists of values. It's a very handy object that lets us group values together in an ordered list. But there's much more to arrays in JavaScript than just the string index and `length` property.

JavaScript has borrowed some of the functions that functional programming languages implement in their standard libraries, and made them a bit more convenient by binding them to the `Array` prototype. In a follow-up post, we'll see how we can apply functional approaches to writing programs that compose better than standard procedural code.

But first, let's dive into the basics.

## Part 1: Search

There are many ways to skin this particular cat, depending on what you want to achieve. Let's take a fun data source that provides a list of things that we can practice our JS-fu on:

{% runkit %}
// we set up the data fetch and hand the data
// to our main function
const fetch = require('node-fetch');

const SOURCE_URL = 'https://www.reddit.com/r/reactiongifs.json';

fetch(SOURCE_URL)
  .then(response => response.json())
  .then(main)
  .catch(err => console.error(err));

// ----[main]----

function main(json) {
  // here's where we deal with the data
  console.log(json.data.children);
}
{% endrunkit %}

We'll be using [`/r/reactiongifs`](https://www.reddit.com/r/reactiongifs/) on Reddit. Run the example above to see what we're dealing with.

_Hint:_ Any Reddit page can be fetched in JSON form by appending the `.json` suffix to the URL. Try it!

### Question: does every list item match a particular criteria?

Say that we wanted to check that every post in the list contains the acronym `MRW` in the `title` field. For this, we use the [`every()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every) function on the list.

{% runkit
// we set up the data fetch and hand the data
// to our main function
const fetch = require('node-fetch');

const SOURCE_URL = 'https://www.reddit.com/r/reactiongifs.json';

fetch(SOURCE_URL)
  .then(response => response.json())
  .then(main)
  .catch(err => console.error(err));
%}
const postTitleContainsMRW = post => post.data.title.includes('MRW');

function main(json) {
  const posts = json.data.children;
  const eachContainsMRW = posts.every(postTitleContainsMRW);
  console.log('Every post contains MRW?', eachContainsMRW);
}
{% endrunkit %}

_Note:_ When the function supplied to `every()` returns `false`, it stops iterating over the array and immediately returns `false`. If all items in the array resolve to `true`, it returns `true`.

### Question: does the list contain _any_ items matching a criteria?

OK, what about if we just want to check if _any_ value matches? Let's look for the word `cat` in the title using [`some()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some).

{% runkit
// we set up the data fetch and hand the data
// to our main function
const fetch = require('node-fetch');

const SOURCE_URL = 'https://www.reddit.com/r/reactiongifs.json';

fetch(SOURCE_URL)
  .then(response => response.json())
  .then(main)
  .catch(err => console.error(err));
%}
const postTitleContainsCat = post => post.data.title.includes('cat');

function main(json) {
  const posts = json.data.children;
  const anyContainsCat = posts.some(postTitleContainsCat);
  console.log('Does any post contain the word cat?', anyContainsCat);
}
{% endrunkit %}

_Note:_ Since this function is the complement of `every()`, it will stop iteration as soon as the first item resolves to `true`. If none of the items resolve to `true`, it returns `false`.

### Question: what's the first item in the list that matches a criteria?

Assuming the answer above was correct (it is dynamic data, after all!), let's find the first post that had the word `cat` in it. For this, we can use [`find()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find).

{% runkit
// we set up the data fetch and hand the data
// to our main function
const fetch = require('node-fetch');

const SOURCE_URL = 'https://www.reddit.com/r/reactiongifs.json';

fetch(SOURCE_URL)
  .then(response => response.json())
  .then(main)
  .catch(err => console.error(err));
%}
const postTitleContainsCat = post => post.data.title.includes('cat');

function main(json) {
  const posts = json.data.children;
  const catPost = posts.find(postTitleContainsCat);
  console.log(catPost);
}
{% endrunkit %}

If no element is found, it returns `undefined`.

### Question: which position is the first found item in?

Just substitute `find()` by [`findIndex()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex) and hey presto!

{% runkit
// we set up the data fetch and hand the data
// to our main function
const fetch = require('node-fetch');

const SOURCE_URL = 'https://www.reddit.com/r/reactiongifs.json';

fetch(SOURCE_URL)
  .then(response => response.json())
  .then(main)
  .catch(err => console.error(err));
%}
const postTitleContainsCat = post => post.data.title.includes('cat')

function main(json) {
  const posts = json.data.children;
  const catPostIndex = posts.findIndex(postTitleContainsCat);
  console.log(catPostIndex);
}
{% endrunkit %}

## Part 2: Transformation

So far, the methods described above only scan the contents, but other more useful methods allow us to transform an array into something else. Let's start with the basics, though.

### Task: get a list of posts matching a criteria

Previously, we only cared about a single (first) value in the array. What about the rest? [`filter()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) allows you to do just that.

{% runkit
// we set up the data fetch and hand the data
// to our main function
const fetch = require('node-fetch');

const SOURCE_URL = 'https://www.reddit.com/r/reactiongifs.json';

fetch(SOURCE_URL)
  .then(response => response.json())
  .then(main)
  .catch(err => console.error(err));
%}
const postTitleContainsCat = post => post.data.title.includes('cat');

function main(json) {
  const posts = json.data.children;
  const postsWithCats = posts.filter(postTitleContainsCat);
  console.log(postsWithCats);
}
{% endrunkit %}

### Task: convert each item in the array

Sometimes we need to take an object and transform it into a different format to be consumed by some other component or function. In this case, we can use the [`map()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) function.

{% runkit
// we set up the data fetch and hand the data
// to our main function
const fetch = require('node-fetch');

const SOURCE_URL = 'https://www.reddit.com/r/reactiongifs.json';

fetch(SOURCE_URL)
  .then(response => response.json())
  .then(main)
  .catch(err => console.error(err));
%}
const simplifyPost = post => ({
  title: post.data.title,
  image: post.data.thumbnail,
  animation: post.data.url
});

function main(json) {
  const posts = json.data.children;
  const simplerPosts = posts.map(simplifyPost);
  console.log(simplerPosts);
}
{% endrunkit %}

_Note:_ `map()` returns a new array of items without changing the original array.

### Task: create a summary of the list of items

If you need to produce any kind of summation, rollup or transformation on a list of items, [`reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) is the way to go. The gist of this operation is that you give it an initial value, and the function supplied to it will return the next value after processing each item in turn.

For this example, let's create a `Set` of all words used in the title. `Set`s are quite useful as they take care of deduplication of items that are already in the set.

{% runkit
// we set up the data fetch and hand the data
// to our main function
const fetch = require('node-fetch');

const SOURCE_URL = 'https://www.reddit.com/r/reactiongifs.json';

fetch(SOURCE_URL)
  .then(response => response.json())
  .then(main)
  .catch(err => console.error(err));
%}
const addWordsFromTitle = (set, post) => {
  // we lowercase the title first
  const title = post.data.title.toLowerCase();
  // we split along every word boundary which isn't an apostrophe
  const words = title.split(/[\^\w']+/);
  // for each non-empty word, we add it to the set
  words.filter(word => word.length > 0)
    .forEach(word => set.add(word));
  // IMPORTANT: we return the set as the next value
  return set;
};

function main(json) {
  const posts = json.data.children;
  // NOTE: here we start with an empty set and add words to it
  const allWords = posts.reduce(addWordsFromTitle, new Set());
  console.log(allWords);
}
{% endrunkit %}

This is a *very* powerful transformational method and can express almost any kind of operation you can think of, including all of the ones described above! If you want a quick taste of things you can do with just `reduce` (or `fold`, as it's called in functional languages), have a look at Brian Lonsdorf's talk below:

{% youtube JZSoPZUoR58 %}

### Task: order the items within a list

If we want to sort arbitrary values, we need to provide a comparator so that we can tell the sorting algorithm about ordering. To do this, we need to provide a function that takes two items from the array and returns one of three values:

  - `-1`: when the first item should be before the second item (any negative number will do)
  - `0`: when the two items are equivalent in order
  - `1`: when the second item should come before the first item (any positive number will do)

Let's sort the items based on title length in decreasing order (longest first). If two titles have the same length, order them alphabetically.

{% runkit
// we set up the data fetch and hand the data
// to our main function
const fetch = require('node-fetch');

const SOURCE_URL = 'https://www.reddit.com/r/reactiongifs.json';

fetch(SOURCE_URL)
  .then(response => response.json())
  .then(main)
  .catch(err => console.error(err));
%}
const comparePosts = (a, b) => {
  const titleA = a.data.title.toLowerCase();
  const titleB = b.data.title.toLowerCase();
  if (titleA.length > titleB.length) return -1;
  if (titleA.length < titleB.length) return 1;
  return titleA.localeCompare(titleB, 'en', { sensitivity: 'base' });
};

function main(json) {
  // Array.from() creates a copy of the array so that we don't
  // modify the original data
  const posts = Array.from(json.data.children);
  posts.sort(comparePosts);
  console.log(posts);
}
{% endrunkit %}

_Note:_ `sort()` sorts the array in-place, which means that the original array is modified.

## Wrapping up

This post covers just the basics of array methods that we'll need when we start implementing a more functional approach in our examples. Until then, keep in mind that whenever you feel the need to write a `for` loop over an `Array`, there's probably a way to write that same thing using the methods described above.

Stay curious!