NRF
{BI} blog

{BI} blog

Stop using arrow functions everywhere ...

NRF's photo
NRF
ยทNov 2, 2021ยท

8 min read

Ever since their inclusion in JavaScript, arrow functions' usage has increased at a very rapid rate by JavaScript developers. Everyone seems to like arrow functions; perhaps more than they really should.

Firstly, there are situations where one absolutely should not use an arrow function.

Before going further, it is important to understand that arrow functions are not just a syntactical sugar for normal functions. Arrow functions, apart from having a different syntax for declaration, also behave differently than normal functions. One of the key differences is that they have no knowledge of the execution context. The execution context is dynamic (assuming your application has function calls) and is accessed through the this keyword in JavaScript. This is why it is often said that arrow functions do not have their own binding to this. They inherit this from their enclosing scope. This difference is at the heart of understanding when not to use arrow functions.

window.id = 107;

const obj = {
  id: 108,
  normalFunction: function () { console.log(this.id) },
  arrowFunction: () => { console.log(this.id) },
}

obj.normalFunction(); // output: 108
obj.arrowFunction(); // output: 107

The reason arrowFunction() logs 107 is that its declaration (as a method of obj) is in the global scope which, in the case of a browser environment, is the window object. So this resolves to window inside arrowFunction().

Do not use arrow functions ...

... when defining object methods

Using arrow functions to define object methods may result in unexpected (buggy?) behavior. Chances are that if you're defining an object method, you need access to to its properties from within that method.

const counter = {
  count: 0,
  currentCount: function () { return this.count },
  incrementCount: function () { ++this.count }
}

counter.incrementCount();
console.log(counter.currentCount()); // output: 1

Everything is good and works as expected. But, if we had used arrow functions here:

const counter = {
  count: 0,
  currentCount: () => { return this.count },
  incrementCount: () => { ++this.count }
}

counter.incrementCount(); // this does nothing unfortunately
console.log(counter.currentCount()); // output: NaN

this resolves to the window object in the above case and window.count doesn't exist. If it is conciseness you're after, you can use the new shorthand syntax for defining object methods. The code below is equivalent to the first example above:

const counter = {
  count: 0,
  currentCount() { return this.count },
  incrementCount() { ++this.count }
}

counter.incrementCount();
console.log(counter.currentCount()); // output: 1

... when defining prototype methods

This case is conceptually similar to the one discussed above. If you wish to access an instance property, you must not use an arrow function when defining the prototype method.

function Person(name) {
  this.firstName = name;
}

Person.prototype.normalGreet = function () {
  console.log(`Hi ${this.firstName}`);
}

Person.prototype.arrowGreet = () => {
  console.log(`Hi ${this.firstName}`)
}

const person1 = new Person("Jack");

person1.normalGreet(); // output: "Hi Jack"
person1.arrowGreet(); // output: "Hi undefined"

As you can see, this.firstName inside the normal function resolves successfully to "Jack" while in the case of the arrow function it returns undefined. This is because inside the arrow function this points to the function's enclosing scope which is the window object in this case. Since window.firstName is undefined, it returns undefined.

... when defining any function that depends on the dynamic context

The recurring theme is that arrow functions have no knowledge of the (dynamic) execution context and, hence, should not be used when the execution context is a factor. Another example of this would be callbacks that depend on the dynamic context like event handlers.

someButton.addEventListener("click", () => {
  console.log(this.innerText);
})

In the code example above, clicking on someButton will log undefined since this does not refer to the currentTarget.

Furthermore, call, apply, and bind do not work with arrow functions.

... when defining a constructor function

Arrow functions can't be used as a constructor function (can't use new with them). It will simply raise an error.

const Person = (name) => {
  this.firstName = name;
}

const person1 = new Person(); // TypeError: Person is not a constructor

... if you wish to use the special arguments object

Just like with this, arrow functions do not have their own arguments object. Instead, arguments inside an arrow function refers to the arguments of the enclosing scope.

function greet(name) {
  const constructGreeting = (greeting) => {
    return `${greeting} ${arguments[0]}`;
  }

  return constructGreeting("hello");
}

console.log(greet("Jack")); // output: "hello Jack"

As you can see, arguments[0] inside the arrow function constructGreeting() actually resolves to the value of name which is the argument to greet() (the function that encloses constructGreeting()).

It's not all bad

There are scenarios where arrow functions provide benefit. Mostly by making the code cleaner/intuitive.

Async callbacks

Imagine you have async code but you still need access to the original context that called the async function (from within the async callback). Normal functions wouldn't work here (if you used this inside the async callback). This issue was previously solved by using bind or declaring a self variable where self = this. But since this is already lexically bound in an arrow function, the arrow function solves this problem elegantly. An example should clear things up.

const obj = {
  firstName: "Jack",
  useName: function () {
    setTimeout(function () {
      console.log(`Hi ${this.firstName}`);
    }, 1000);
  }
}

obj.useName(); // output after 1 second: "Hi undefined"

In the example above, I've used setTimeout() to simulate asynchrony. The callback function passed to setTimeout() is called after 1 second of calling obj.useName(). By the time the callback function is called, useName() has long been executed. In fact, the callback queue is executed in the global context (window in case of our browser environment). This means that this = window when the async callback is called. This is why this.firstName resolves to undefined.

As I stated above, before arrow functions, this problem was solved by:

  • using bind

    const obj = {
      firstName: "Jack",
      useName: function () {
        setTimeout(function () {
          console.log(`Hi ${this.firstName}`);
        }.bind(this), 1000);
      }
    }
    
    obj.useName(); // output after 1 second: "Hi Jack"
    
  • by declaring a referencing variable (traditionally called self)

    const obj = {
      firstName: "Jack",
      useName: function () {
        let self = this;
        setTimeout(function () {
          console.log(`Hi ${self.firstName}`);
        }, 1000);
      }
    }
    
    obj.useName(); // output after 1 second: "Hi Jack"
    

Now we can simply use an arrow function which gives us the expected results.

const obj = {
  firstName: "Jack",
  useName: function () {
    setTimeout(() => {
      console.log(`Hi ${this.firstName}`);
    }, 1000);
  }
}

obj.useName();

Callbacks for iterator functions

When I say iterator functions I mean functions like map(), reduce(), and forEach() that go through each item of an iterable and feed that item to a callback function. This is where arrow functions really shine. No need to worry about the dynamic context. Additionally, the compact syntax and the implicit return feature of arrow functions often result in code that is concise and more readable at the same time.

const myArray = [2, 4, 6, 8];

const newArray = myArray.map(number => number * 2);

console.log(newArray); // output: [4, 8, 12, 16]

In my humble opinion ...

Although arrow functions are a great addition to JavaScript, and in the proper context their concise syntax improves code readability, I still don't think that they should be the default for function declaration. If the situation doesn't specifically call for it, there is no need to use an arrow function.

Consider, for example, your globally-scoped or module-scoped functions (perhaps you have a helpers.js file that has a bunch of helper functions your application needs). The arrow function syntax in this case simply looks more cluttered than a normal function declaration. On top of it, the code becomes even more cluttered if you have async functions and need to export some of them.

export async function foo() {
  // function body
}

export const bar = async () => {
  // function body
}

Using arrow functions:

export const firestore = firebase.firestore();
export const auth = firebase.auth();
const provider = new firebase.auth.GoogleAuthProvider();

export const signInWithGoogle = () => auth.signInWithPopup(provider);

export const getUserDocument = async (uid) => {
  // body
}

export const createUserDocument = async (user, additionalData) => {
  // body
}

Using normal functions:

export const firestore = firebase.firestore();
export const auth = firebase.auth();
const provider = new firebase.auth.GoogleAuthProvider();

export function signInWithGoogle() {
  auth.signInWithPopup(provider);
}

export async function getUserDocument(uid) {
  // body
}

export async function createUserDocument(user, additionalData) {
  // body
}

The normal function syntax is just better. It reads better and it is actually less typing. Not that less typing should be a factor here but I've often observed that it is a factor behind using arrow functions.

Another example, which is slowly becoming a pet peeve of mine, is using arrow functions for declaring a React funtional component. There is just no need to do that! You can simply use a normal function declaration.

Using an arrow function:

export default const UserCard = (props) {
  // a ReactJS functional component -- arrow edition
}

Using a normal function:

export default function UserCard(props) {
  // a ReactJS functional component -- normal edition
}

Isn't a normal function delcaration much clearer?


๐Ÿ‘‰๐Ÿป Follow me on twitter: click here

๐Ÿ‘‡๐Ÿป Subscribe to my newsletter


ย 
Share this