DEV Community

Samuel Rouse
Samuel Rouse

Posted on

Higher-order Functions You Already Use

You may not hear much about higher-order functions (HoFs), but there are plenty of examples in everyday JavaScript. Let's take a quick look at how they are used and some common patterns.

Callbacks & HoFs

Higher-order functions accept or return a function. While they can take many shapes, most of the time we are dealing with functions that accept callbacks. If you've ever passed a callback to a function, you've used this functional programming concept!

Events

Perhaps the most common HoFs are for events. It doesn't matter whether you use a library, a framework, or write in plain JavaScript; event handlers are almost always callbacks passed to a higher-order function like addEventListener.

const myButton = document.getElementById('myButton');
myButton.addEventListener('click', (event) => {
  console.log('This is a callback function!');
  console.log('We passed this to addEventListener.');
  console.log('You clicked', event.target);
});

// Or in JSX
const Button = () => (<button onClick={
  () => console.log('I\'m a callback!')
}>Test</button>);

Enter fullscreen mode Exit fullscreen mode

What's in a Parameter Name?

An Event is passed as the first argument, no matter what you name it.

// Some people prefer "e" or "evt"
myButton.addEventListener('click', (e) => {
  console.log('You clicked', e.target);
});

// The name is your choice. The value is always an Event
myButton.addEventListener('click', (orange) => {
  console.log('You clicked', orange.target);
});
Enter fullscreen mode Exit fullscreen mode

It's still there even if you don't give it a name!

myButton.addEventListener('click', function () {
  // "arguments" is not available for arrow functions
  // but the first argument is still an Event
  // even if you don't use it.
  console.log('You clicked', arguments[0].target);
});
Enter fullscreen mode Exit fullscreen mode

But the callback function doesn't have to be anonymous. Once you know the arguments a callback receives – like an Event as the first argument no matter what – you can feel more confident splitting up the code.

const myButton = document.getElementById('myButton');
const theButtonAction = (event) => {
  console.log('This is still a callback function!');
  console.log('We passed this to addEventListener.');
  console.log('You clicked', event.target);
};

// This will pass an Event to myButtonAction
myButton.addEventListener('click', theButtonAction);
Enter fullscreen mode Exit fullscreen mode

This shows us one of the more powerful parts of HoFs: callbacks can be reused rather than creating new ones each time.

const myButton = document.getElementById('myButton');
const yourButton = document.getElementById('yourButton');

// Re-use the event handler on two different buttons
myButton.addEventListener('click', theButtonAction);
yourButton.addEventListener('click', theButtonAction);
Enter fullscreen mode Exit fullscreen mode

Where Everybody Knows Your Name

In fact, this is essential to another event-handling HoF: removeEventListener. We need a reference to the original function if we want to remove it.


const myButton = document.getElementById('myButton');
const theButtonAction = (event) => {
  console.log('This is a callback function!');
  console.log('We passed this to addEventListener.');
  console.log('You clicked', event.target);
  // Stopping the event handler needs the original function
  myButton.removeEventListener('click', theButtonAction);
};

myButton.addEventListener('click', theButtonAction);
Enter fullscreen mode Exit fullscreen mode

Our earlier example has no reference to the callback, so we cannot use removeEventHandler. That handler accepts clicks as long as the element exists. In this newest example, the event handler only runs once because we are able to remove it.

Timeouts and Intervals

Timeouts feature in a lot of coding examples because they are an easy way to cause asynchronous events or simulate more complex behavior. These are also higher-order functions that take a callback.

setTimeout(() => console.log('After ~100 ms!'), 100);
setInterval(() => console.log('Every ~100 ms'), 100);
Enter fullscreen mode Exit fullscreen mode

If you are unfamiliar with it, I use the tilde (~) to indicate approximately 100 milliseconds because timers & intervals are not precise.

Array Operations

Many references to HoFs and Functional Programming in JavaScript talk about the Array prototype methods like .map() and .reduce(). These can be very useful for processing data.

// Double
[ 1, 2, 3 ].map((value) => value * 2);
// [ 2, 4, 6 ]

// Sum
[ 1, 2, 3 ].reduce((accumulator, value) => value + accumulator, 0);
// 6

// Double and Sum (with chaining!)
[ 1, 2, 3 ]
  .map((value) => value * 2)
  .reduce((accumulator, value) => value + accumulator, 0);
// 12
Enter fullscreen mode Exit fullscreen mode

These are also great examples of places where we can name and re-use functions.

const double = (value) => value * 2;
const sum = (value1, value2) => value1 + value2;

[ 1, 2, 3 ].map(double);
// [ 2, 4, 6 ];

[ 1, 2, 3 ].reduce(sum, 0);
// 6;

// Double and Sum (with chaining!)
[ 1, 2, 3 ]
  .map(double)
  .reduce(sum, 0);
// 12
Enter fullscreen mode Exit fullscreen mode

Parameter Pairings

Knowing the parameters passed to callbacks for these HoFs is important to getting the correct response. The sum function won't work with .map() because map doesn't provide two values.

React

Many of the React hooks are HoFs, like useCallback, useEffect, useMemo, and useReducer.

useEffect allows you to write your own HoF as well, because the function you pass to useEffect can return a function which is called for cleanup.

useEffect(() => {
  // This function is passed to useEffect
  doSomething();

  // Our function returns a function, too!
  return () => {
    cleanupSomething();
  }
});
Enter fullscreen mode Exit fullscreen mode

Of course, these don't have to be anonymous, though dependencies and consuming other hooks can complicate things.

const someEffect = () => {
  doSomething();
  return cleanupSomething;
};

useEffect(someEffect);
Enter fullscreen mode Exit fullscreen mode

Samples & Suggestions

Try looking for anonymous callbacks like these in code samples and articles:

// Anonymous timeout
setTimeout((result) => doSomething(result), 100);

// Anonymous promise chain
Promise.resolve(data)
.then((result) => doSomething(result));

// Anonymous event handler
foo.addEventListener((event) => handleEvent(event));

// Anonymous array operation
data.map((row) => reformatRow(row));
Enter fullscreen mode Exit fullscreen mode

When you find them, ask yourself if you need that wrapper at all:

// Not passing any arguments, so it's fine
setTimeout(doSomething, 100);

// Safe. Promises only pass one argument.
Promise.resolve(data)
.then(doSomething);

// Safe. Event handlers receive one argument.
foo.addEventListener(handleEvent);

// Depends. Does reformatRow accepts other arguments?
data.map((row) => reformatRow(row));
Enter fullscreen mode Exit fullscreen mode

Limitations

We can't eliminate a wrapper function in every case. Some HoFs like Array.prototype.map provide multiple arguments to their callback – value, index, array in this case – and some functions operate differently when they get more arguments. The classic example is parseInt, which accepts a base as the second argument. If you pass it directly to .map it will receive the index as the second argument, producing unexpected outcomes.

['9', '10', '11', '12', '13'].map(parseInt);
// [ 9, NaN, 3, 5, 7 ]

['9', '10', '11', '12', '13'].map((val) => parseInt(val));
// [ 9, 10, 11, 12, 13 ]
Enter fullscreen mode Exit fullscreen mode

It's important to know what arguments are passed to your functions, and what arguments they accept.

Feedback

Thanks for reading, and let me know what you thought. Do you have any tips or suggestions for developers learning to understand higher-order functions? Comment or post a link to other articles!

Top comments (0)