Creating & Using Functions in React: ES5 vs ES6

by Anders Wood

March 5, 2019

When creating, referencing, calling and passing functions within and between React components, you have myriad options and combinations of options at your fingertips, it can be overwhelming! For the most part, you can get by using a combination that “works,” but eventually you may encounter a scenario where your combination doesn’t just “work,” leaving you flummoxed and questioning basic, life decisions.

The following provides basic guidance around creating, referencing, calling and passing common types of functions in a React component. The content is intended for beginner/intermediate React users that want more context around which type of function to create and how to use it. I think I would have found this kind of summary/explanation useful a year ago when I learned React and only barely knew JavaScript.

In this Post:

At Brandfolder, we employ ES6 in all React components; this avoids .bind and keeps us all on the same page. In general, consistency and predictability are paramount across a codebase: so pick a strategy and stick to it. All of these types of functions will work well when employed correctly and given the proper support for ES6 when needed.

Three common functions in a React component:

handleSubmit = () => {}      <-- ES6 arrow function

handleSubmit() {}            <-- React class component function

function handleSubmit () {}  <-- ES5 function

The ES6 function preserves the context or scope of the function’s contents (thorough explanation here). Depending on the contents of the function, this context preservation may have implications and require you to handle the function differently when passing it around (see details below). The React class component function and ES5 behave very similarly.

Three ways to reference functions within a React component:

ES6 callback function:
<button onClick={(e) => this.handleSubmit(e)}></button>

Referenced equation without parentheses:
<button onClick={this.handleSubmit}></button>

ES5 callback function:
<button onClick={function(e) {this.handleSubmit(e)}}></button>

These three ways all behave very similarly with minimal differences between them. Importantly, in these three cases we’re referencing the handleSubmit function in the jsx, and not invoking or calling the handleSubmit function like so: this.handleSubmit().

Four ways to pass a function as a prop:

ES6 callback function:
<ChildComponent handleSubmit={() => this.handleSubmit(data)}/>

Referenced equation without parentheses:
<ChildComponent handleSubmit={this.handleSubmit}/>

ES5 callback function:
<ChildComponent handleSubmit={function () {this.handleSubmit(data)}}
/>

Referenced equation without parentheses with .bind(this):
<ChildComponent handleSubmit={this.handleSubmit.bind(this, arg)}/>

These ways of passing a function as a prop are similar to the three ways of referencing a function because we’re doing the same thing: passing the function around without actually calling or invoking it.

Employing .bind(this)… and not employing it

The glaring difference between all the ways to pass a function as a prop above is in the last example where we’re using .bind(this, arg). The .bind preserves the context of this so that when the function is invoked down the road, wherever that may be, the function and its contents will behave as if they are in the same place as where the function was first instantiated. After the this in the .bind, you can pass in arguments, e.g. this.handleSubmit.bind(this, arg1, arg2, arg3).

The .bind is usually used in tandem with ES5 functions as a way of preserving the function’s context. Since ES6 functions preserve the context already and you do not need the .bind.

If there are no variables or references specific to the context of the function, preserving the context doesn’t matter! For example:

function handleSubmit(newUserEmail) {
  fetch('/api/users/signup', {
    method: 'POST',
    body: JSON.stringify({email: newUserEmail})
  })
}

...

<ChildComponent handleSubmit={function () {this.handleSubmit(data)}} />

No need to include .bind(this) when passing this ES5 function as a prop to ChildComponent. You can call this function anywhere in your code or in different functions, and as long as you pass in a new user email as an argument, it will perform the same, regardless of where it is.

On the other hand, if this function included some contents for which the context or scope was important, you should consider using .bind(this) when passing the function along. For example:

function handleSubmit(newUserEmail) {
  if (this.validateEmail(newUserEmail)) {
    fetch('/api/users/signup', {
      method: 'POST',
      body: JSON.stringify({email: newUserEmail})
    })
  }
}

validateEmail(email) {
  if (email.indexOf('@') && email.indexOf('.')) {
    return true;
  } else {
    return false;
  }
}

...

<ChildComponent handleSubmit={function (){
  this.handleSubmit.bind(this, data);
}}/>

Because the handleSubmit function invokes another function in the same component using this.validateEmail, you need to include .bind(this) so that wherever the function is invoked, the context of this is preserved.

Alternatively, you can use an ES6 function and skip the .bind(this):

handleSubmit = newUserEmail => {
  if (this.validateEmail(newUserEmail)) {
    fetch('/api/users/signup', {
      method: 'POST',
      body: JSON.stringify({email: newUserEmail})
    })
  }
}

validateEmail(email) {
  if (email.indexOf('@') && email.indexOf('.')) {
    return true
  } else {
    return false
  }
}

...

<ChildComponent handleSubmit={() => {this.handleSubmit(data)}}/>

Ahh.. much more concise. Here, the handleSubmit arrow function freezes the meaning of this so that down the road, when you call handleSubmit — the this will have the same meaning as it did where the function is instantiated.

Aside: Creating a Pure, React Component is an optimization that may be worth exploring. I won’t go into detail here, but the gist of it is that if you avoid instantiating ES6 arrow functions and .bind(this) in the render function, you can avoid unnecessary renders in pure, children components. Corey House provides a great example here.

Which Way is Best?!

When in doubt: be consistent! Develop a strategy by deliberately selecting the type of function to instantiate, a way to reference it and a way to pass it as props. Avoiding arbitrary choices for these actions throughout a React app will lead to more maintainable and consistent code.

I work at Brandfolder and have had some nice ah-ha moments understanding these different types of functions in React in the last year. Give a 👏 if you found this helpful and please pass along feedback, especially if I got something wrong or could explain something better!