Closures are an important concept to understand in JavaScript, but how can we practically use them to improve our code?
Example of a closure
Let’s take a look at the following example of a closure.
function handleLikePost(step) {
let likeCount = 0;
return function addLike() {
likeCount += step;
return likeCount;
};
}
const doubleLike = handleLikePost(2);
console.log(doubleLike()); // 2
console.log(doubleLike()); // 4
console.log(doubleLike()); // 6
The inner function, addLike
, closes over the likeCount
variable. In other words, this closure allows us preserve state of our likeCount
variable between function calls.
Also, since we are passing in this step argument to the outer function, it is also kept around through a closure. We haven’t realized the full benefit of this pattern yet, which enables us to keep the step value passed to the outer function around for the next function call.
What is partial application?
This approach to use higher order functions (functions passed to other functions) to preserve data through closures is called partial application.
Partial application refers to the fact that we’re applying some, but not all of the arguments of a function and waiting for the rest of the arguments. But what we haven’t done in this example is pass any arguments to the double like function. What would our code look like if we did this?
Let’s say we are in the process of building a social media application.
We’re already keeping track of likes with this handleLike function, but before that, let’s say we need to get our user’s posts and comments from an external API.
For this function, we can get the appropriate data we need by providing a given url and the type of data that we need from a given endpoint. Let’s say we need to get our posts and comments in multiple pages in our app. As a result, we have to pass in our baseUrl and endpoint every time we use it.
function getData(baseURL, endpoint) {
fetch(`${baseURL}${endpoint}`)
.then((res) => res.json())
.then((data) => data);
}
getData("https://jsonplaceholder.typicode.com", "/posts");
getData("https://jsonplaceholder.typicode.com", "/comments");
Since our app depends on these two data types, even though we’re using a function that helps us cut down on our repetition, we need to provide our baseUrl every time we call it.
The benefit of partially applied functions
Here’s the benefit of higher order functions to us as developers—they let us have functions with certain values that are preserved, so to speak. With this, it enables us to make our functions more clear as to what they do. They let us write better code by allowing our functions to have single responsibilities.
Let’s rewrite this example with partial application. The first thing we can do is write a function within getData
. We don’t have to give this function a name like we did before. We just want to return it:
function getData(baseURL, endpoint) {
return function () {
fetch(`${baseURL}${endpoint}`)
.then((res) => res.json())
.then((data) => data);
};
}
And now instead of having the getData
function accept both arguments, baseURL
and endpoint
, have the inner function accept endpoint
.
function getData(baseURL) {
return function (endpoint) {
fetch(`${baseURL}${endpoint}`)
.then((res) => res.json())
.then((data) => console.log(data));
};
}
Just a quick question—when we call getData
once what will we get?
We’ll get our inner anonymous function returned to us. And when we do that, we can assign this generic getData
function to something more specific. We’re using the following baseURL
to get post and comment data: jsonplaceholder.typicode.com, we could put it in a variable called getSocialMediaData
:
const getSocialMediaData = getData("https://jsonplaceholder.typicode.com");
At this point, we can already see an immediate benefit. By partial application, when we lock in this baseURL
value through a closure, we get a more clearly defined, single responsibility for this function, which results in a more clear name. Whenever we use it, we know exactly what we’ll be doing—getting social media data.
How do we use it?
All we have to do is call it, now with the argument required for the route. Let’s try the route needed to get comments data, /comments:
const getSocialMediaData = getData("https://jsonplaceholder.typicode.com");
getSocialMediaData("/comments");
// (500) [{…}, {…}, {…}, {…}, {…}]
And when we call it, we see all of our comments. If we wanted to get our posts, what would we do?
We don’t need to provide our baseUrl anymore. That’s being saved through getData’s closure. But we do need to pass the differing route for the post data, /posts:
getSocialMediaData("/posts");
// (100) [{…}, {…}, {…}, {…}, {…}]
We can reuse this partially applied getSocialMediaData
function for as many routes as we need.
What is the takeaway for partial application?
With a partially applied function, you pass some arguments and you get back a function that locks those argument values in place through a closure and can call with some other data.
In other words, a partially applied function reduces the total number of arguments for a function, while giving us a pattern for functions to remember data passed to it.
Extending the example
But we can extend this even further. Right now, as you can see, in this then callback, we’re just taking the data that we get and logging it to the console. Naturally within our app, we want a way to display that to our users. Is there a way that we can extend our partially applied function for it to accept a callback so we can manipulate the data as we see fit?
We could just add another anonymous inner function, before fetching the data and pass through a callback function, that will be called in the inner body and therefore will wrap our final data:
function getData(baseUrl) {
return function (route) {
return function (callback) {
fetch(`${baseUrl}${route}`)
.then((res) => res.json())
.then((data) => callback(data));
};
};
And also make sure to return this most inner function. So what can we do now in executing our partially applied function?
Now instead of getting data, when we call getSocialMediaData('/posts')
, we get a returned function that takes a callback. So let’s assign this inner function to a new variable, getSocialMediaPosts
, when using the posts route and getSocialMediaComments
for the comments route:
const getSocialMediaComments = getSocialMediaData("/comments");
const getSocialMediaPosts = getSocialMediaData("/posts");
Then what can we do? We can pass a callback function to both of these new functions, and since we are getting their data in the form of arrays in both cases, we could iterate over both arrays using the .forEach()
method and maybe we just want their title in either case, so we’ll just console.log
each comment’s title.
In the real-world we would display them in our app:
const getSocialMediaPosts = getSocialMediaData("/posts");
getSocialMediaPosts((posts) => {
posts.forEach((post) => console.log(post.title));
});
And finally, let’s see what our partially applied function would look like as an arrow function. See if you can convert these functions to a series of arrow functions if you can. We just need to remove the function keyword and the return keywords, plus the parentheses around parameters, and the curly braces and we can put everything on one line and it’ll work as before:
const getData = (baseUrl) => (route) => (callback) =>
fetch(`${baseUrl}${route}`)
.then((res) => res.json())
.then((data) => callback(data));
Some JS developers like writing their higher order functions this way, but I find the previous style better to understand. I would try to understand both and use whichever is more readable for you.
Summary
This might all be a little hard to wrap your head around, so I would recommend playing around with this example or our previous handleLike
example so you can better understand what’s going on here, the order in which we call these functions, and what we can do with such higher order function patterns.
The takeaway is that now instead of having one function do multiple things for us, partial application lets our functions have just single, clearly defined responsibilities.
Know that partial application isn’t a technique you reach for very often, but it’s a powerful tool to improve the role of our functions, their reusability and separation of concerns.
Become a React Developer in 5 Weeks
React is hard. You shouldn't have to figure it out yourself.
I've put everything I know about React into a single course, to help you reach your goals in record time:
Introducing: The React Bootcamp
It’s the one course I wish I had when I started learning React.
Click below to try the React Bootcamp for yourself:
Top comments (0)