Correct use of arrow functions in React

2019-01-17 00:41发布

问题:

I am using ReactJS with Babel and Webpack and using ES6 as well as the proposed class fields for arrow functions. I understand that arrow functions make things more efficient by not recreating the functions each render similar to how binding in the constructor works. However, I am not 100% sure if I am using them correctly. The following is a simplified section of my code in three different files.

My code:

Main.js

prevItem = () => {
    console.log("Div is clicked")
}

render(){
    return (
         <SecondClass prevItem={this.prevItem} />
    )
}

SecondClass.js

<ThirdClass type="prev" onClick={()=>this.props.prevItem()} />

ThirdClass.js

<div onClick={()=>{this.props.onClick()}}>Previous</div>

Question:

Is my code above using the arrow functions correctly? I noticed that for SecondClass.js I could have also used:

<ThirdClass type="prev" onClick={this.props.prevItem} />

Is there a difference between one method or the other since I used an ES6 arrow function in my original function definition? Or should I be using the arrow syntax all the way through until my last div?

回答1:

I understand that arrow functions make things more efficient by not recreating the functions each render similar to how binding in the constructor works.

This is not true. It depends on where exactly are you using the Arrow function. If Arrow function are used in render method, then they create a new instance everytime render is called just like how bind would work. Consider this example

<div onClick={()=>{this.onClick()}}>Previous</div>

Here each time render is called an anonymous function is created and that function when called, calls this.onClick.

However consider the case below

onClick = () => {
    console.log("Div is clicked")
}

In above case, the arrow function does not recreate function everytime, but binds the context to the React component as An arrow function does not have its own this; the this value of the enclosing execution context is used. once when the class is instantiated. This is similar to how binding works is constructor. This is a part of proposed class fields for arrow functions and it isn't a ES6 feature,

To understand what you wish to ask, you must know that a function gets its context from where it is called. Check this question for more understanding.

In your case, you have used Arrow function to define prevItem and hence it gets the context of the enclosing React component.

prevItem = () => {
    console.log("Div is clicked")
}

render(){
    return (
         <SecondClass prevItem={this.prevItem} />
    )
}

Now in its child, even if you call prevItem with any custom context, using bind or arrow function, prevItem when executed in parent i.e Main.js will get the context of its enclosing React component. And since you just wish to execute prevItem function and do not want to pass any data to this from the child, writing

<ThirdClass type="prev" onClick={()=>this.props.prevItem()} />

and

<div onClick={()=>{this.props.onClick()}}>Previous</div>

is simply useless and will only add to performance implication since new functions are created in SecondClass and ThirdClass everytime. You simply don't need to have these functions defined as arrow function and could just write

<ThirdClass type="prev" onClick={this.props.prevItem} />

and

<div onClick={this.props.onClick}>Previous</div>

since its already binded in the parent.

Now even if you have to pass some additional data to these function from ThirdClass and SecondClass, you shouldn't directly use Arrow function or bind in render. Have a look at this answer on How to Avoid binding in Render method



回答2:

I understand that arrow functions make things more efficient by not recreating the functions each time they are referred to

This is not true.

Arrow functions handles the this context in a lexical way, where "normal" function do it dynamically.

On both of your examples of the inline arrow function, you are creating a new function instance on each render.
This will create and pass a new instance on each render

onClick={() => {}}

On the 3rd example you only have one instance.
This only pass a reference to an already existing instance

onClick={this.myHandler}


As for the benefits of arrow functions as class fields (there is a small down side, i will post it in the bottom of the answer), if you have a normal function handler that needs to access the current instance of the class via this:

myHandler(){
  //  this.setState(...)
}

You will need to explicit bind it to the class.
The most common approach will be to do it in the constructor because it runs only once:

constructor(props){
  super(props);
  this.myHandler = this.myHandler.bind(this);
}

If you use an arrow function as the handler though, you don't need to bind it to the class because as mentioned above, the arrow function use a lexical context for this:

myHandler = () => {
  //  this.setState(...)
}

With both approaches you will use the handler like this:

<div onClick={this.myHandler}></div> 

The main reason for taking this approach:

<div onClick={() => this.myHandler(someParameter)}></div>

Is if you want to pass parameters to the handler beside the native event that get passed, meaning you want to pass a parameter upwards.

As mentioned, this will create a new function instance on each render.
(There is a better approach for this, keep reading).

Running example for such use case:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [{ name: 'item 1', active: false }, { name: 'item 2', active: true }],
    }
  }
  toggleITem = (itemName) => {
    this.setState(prev => {
      const nextState = prev.items.map(item => {
        if (item.name !== itemName) return item;
        return {
          ...item,
          active: !item.active
        }
      });
      return { items: nextState };
    });
  }
  render() {
    const { items } = this.state;
    return (
      <div>
        {
          items.map(item => {
            const style = { color: item.active ? 'green' : 'red' };
            return (
              <div
                onClick={() => this.toggleITem(item.name)}
                style={style}
              >
                {item.name}
              </div>
          
          )})
        }
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

A better approach would be to create component composition.
You can create a child component that wraps the relevant markup, will have it's own handler and will get both the data and handler as props from the parent.

The child component will then invoke the handler that it got from the parent and will pass the data as a parameter.

Running example with child component:

class Item extends React.Component {
  onClick = () => {
    const { onClick, name } = this.props;
    onClick(name);
  }
  render() {
    const { name, active } = this.props;
    const style = { color: active ? 'green' : 'red' };
    return (<div style={style} onClick={this.onClick}>{name}</div>)
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [{ name: 'item 1', active: false }, { name: 'item 2', active: true }],
    }
  }
  toggleITem = (itemName) => {
    this.setState(prev => {
      const nextState = prev.items.map(item => {
        if (item.name !== itemName) return item;
        return {
          ...item,
          active: !item.active
        }
      });
      return { items: nextState };
    });
  }
  render() {
    const { items } = this.state;
    return (
      <div>
        {
          items.map(item => {
            return <Item {...item} onClick={this.toggleITem} />
          })
        }
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Class Fields the down-side:
As i mentioned, there is a small down-side for class fields.
The difference between a class method and a class field is that the class field is attached to the instance of the class (constructor function).
where as the class methods and objects are attached to the prototype.

Hence, if you will have ridiculously large amount of instances of this class you may get a performance hit.

Given this code block:

class MyClass {
  myMethod(){}  
  myOtherMethod = () => {}
}

babel will transpile it to this:

var _createClass = function() {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false;
      descriptor.configurable = true;
      if ("value" in descriptor) descriptor.writable = true;
      Object.defineProperty(target, descriptor.key, descriptor);
    }
  }
  return function(Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
}();

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var MyClass = function() {
  function MyClass() {
    _classCallCheck(this, MyClass);

    this.myOtherMethod = function() {};
  }

  _createClass(MyClass, [{
    key: "myMethod",
    value: function myMethod() {}
  }]);

  return MyClass;
}();


回答3:

So your first approach

<ThirdClass type="prev" onClick={()=>this.props.prevItem()} />

In this you can pass any arguments which are available in ThirdClass to the prevItem function. It's the good way of calling parent functions with arguments.Like this

<ThirdClass type="prev" onClick={()=>this.props.prevItem(firstArgument, secondArgument)} />

Your second approach is

<ThirdClass type="prev" onClick={this.props.prevItem} />

This approach disallows you to pass any ThirdClass specific arguments.

Both the apporaches are right, it just that , it depends on your use case. Both of the approach using es6 arrow function and are right in above mentioned respective scenarios



回答4:

Using JavaScript closure can be a different way to other answers, pay attention to the following codes:

clickHandler = someData => e => this.setState({
  stateKey: someData
});

Now in JSX, you can write:

<div onClick={this.clickHandler('someData')} />

The clickHandler with someData return a function with e argument but it is not used inside clickHandler function. so it works well.

For writing more completely write like below:

clickHandler = someData => () => this.setState({
  stateKey: someData
});

It is no need to e, so why I should write it.



回答5:

Using arrows in your original function definition allows you not to bind the function in your constructor.

If you didn't use an arrow...

prevItem(){
  console.log("Div is clicked")
}

Then you would have to create a constructor a bind it there...

class MyComponent extends Component {
  constructor(props) {
    super(props)
    this.prevItem = this.prevItem.bind(this)
  }

  prevItem() { ... }
}

Using the arrow is easier when you start about because it just works and you don't have to understand what a constructor is and delve into the complexities of this in javascript.

However, performance wise it is better to bind in the constructor. The bind in constructor method will create a single instance of the function and re-use it, even if the render method is called multiple times.