React使用ES6 Class


前言

在React官方的文件中,你是不容易找到使用ES6 Class的方式,來定義React元件的教學或指引。這是一個特別的情況,是官方不認同這樣的作法,還是官方並不建議這樣的作法?但我們現在看到許多教學與範例中,例如像Redux的範例中,都是採行ES6 Class的定義方式,不學的話,你就看不太懂程式碼在寫什麼。像下面這個範例中的語法,是更精簡的元件定義語法:

const MyComponent = props => (
  <div className={props.className} />
);

不過我個人認為,目前因為正處於轉換期間,使用這兩種寫法的程式碼都有,建議你可以開始使用ES6+的方式,但還是要能看得懂ES5的方式,像官方的教學文件都還是ES5的,當然你也應該有能力重構它們為ES6+語法。實際上也沒想像中難了解,只是有很多注意事項而已。

為了區分程式碼的差異,以下把目前使用React.createClass定義元件的方式,稱為"ES5方式"。而用繼承於React.Component,稱於"ES6+方式",因為它也有使用了一些ES7的語法特性。

歷史

現在,網路上也有一部份的教學是來自Facebook內部的工程師,所以可以說,在Facebook內部的確也有正在使用這樣的方式。這個作法根據網路上的搜尋調查,大約是在React 0.13之後開始有人提及,鼓勵使用ES6 Class方式來寫React元件,0.13正式版本的發行時間約是在2015年3月,在此之前就有人在Stack Overflow詢問過如Extending React.js components的問題。如果你看過這篇問答,當時的回答,都是要利用其他的週邊函式庫來協助才能達成。

0.13 beta1的官方文件有以下的內容:

In React 0.13.0 you no longer need to use React.createClass to create React components. If you have a transpiler you can use ES6 classes today.

也給出了幾個範例,其中一個是Counter元件,你可以在Redux的範例中看到這個範例的影子:

export class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: props.initialCount};
  }
  tick() {
    this.setState({count: this.state.count + 1});
  }
  render() {
    return (
      <div onClick={this.tick.bind(this)}>
        Clicks: {this.state.count}
      </div>
    );
  }
}
Counter.propTypes = { initialCount: React.PropTypes.number };
Counter.defaultProps = { initialCount: 0 };

在2015年10月時,React 0.14,更加入了一種更精簡的元件定義語法,它只適合stateless(無狀態)的元件,用的是箭頭函式的方式,官方的範例如下:

// A functional component using an ES2015 (ES6) arrow function:
var Aquarium = (props) => {
  var fish = getFish(props.species);
  return <Tank>{fish}</Tank>;
};

// Or with destructuring and an implicit return, simply:
var Aquarium = ({species}) => (
  <Tank>
    {getFish(species)}
  </Tank>
);

// Then use: <Aquarium species="rainbowfish" />

此外,約在2015年時,網路上開始有人提出,區分出React的元件為兩大類,這個區分法並不是單純的以原先React中的state與stateless來區分,而且由整體程式的運作(加入Flux資料流後)來區分。以下是相關的文章:

實際上也有很多種說法:Fat and Skinny, Smart and Dumb, Stateful and Pure, Screens and Components, Logic and Presentational。

註:Redux中所有的範例都是使用ES6+語法方式,它與React或原本官方的Flux範例,不太一樣。你如果要使用或看得懂它的範例,也是需要先學習一些必備知識。

關於Mixins(混用)在ES6+語法無法使用的問題,目前已經有High Order Component的解決方式,但它又是另一項很需要再研究的課題。可以參考:

理由

ES5方式即使用React.createClass,而ES6+語法方式即使用class X extends React.component。基本上它們是兩種在Javascript中類別定義的兩種方式,factory pattern(工廠模式),對比constructor pattern(建構子模式),語法有很大的不同。

使用ES5語法方式的理由如下,解說可以看下面範例:

  • 如果你也不使用JSX的情況下,就是純粹的ES5語法,可以不用依賴如Babel的工具
  • 可以使用Mixins(混入)
  • 方法會Autobinding(自動綁定)

註:"官方教學與網路教學目前大部份都用這個語法"。這一點大概是其他一個理由。

使用ES6+語法方式的理由:

  • 可以使用各種ES6+新式語法,語法更為簡單,閱讀性高
  • 不想再使用Autobinding(自動綁定),因為它算是一個隱性機制,容易造成誤解或意義不明
  • 可以結合如TypeScript或CoffeeScript等語言來使用

註:除了你繼承React.Component之外,如果你還想繼承出自己的元件,並沒有你想像中那麼簡單。大部份的建議都是"use composition over inheritance"(用合成取代繼承)

語法比較

基本結構

這兩個語法的基本結構就不太一樣:

// ES5語法方式
var Photo = React.createClass({
  handleDoubleTap: function(e) { … },
  render: function() { … },
});
// ES6+語法方式
class Photo extends React.Component {
  handleDoubleTap(e) { … }
  render() { … }
}

仔細看一下兩種定義的語法的差異,ES6+語法方式中的方法,少了冒號(:)、function和區分每個方法的逗號(,)。React.createClass本身是一個方法,你在這裡面輸入的,是給它這個方法用的參數值。class X extends React.Component定義出來的,是一個Javascript中的通常類別。

再舉一個例子,是針對React 0.14後的stateless元件的定義方式,這也很常在Redux裡看到:

// ES5語法方式
var MyComponent = React.createClass({
  render: function() {
    return <div className={this.props.className}/>;
  }
});
// ES6+語法方式
class MyComponent extends React.Component {
  render() {
    return <div className={this.props.className}/>;
  }
}
// React 0.14之後,ES6+語法方式
const MyComponent = props => (
  <div className={props.className}/>
);

propTypes 與 getDefaultProps

// ES5語法方式
var Video = React.createClass({
  getDefaultProps: function() {
    return {
      autoPlay: false,
      maxLoops: 10,
    };
  },
  propTypes: {
    autoPlay: React.PropTypes.bool.isRequired,
    maxLoops: React.PropTypes.number.isRequired,
    posterFrameSrc: React.PropTypes.string.isRequired,
    videoSrc: React.PropTypes.string.isRequired,
  },
  render: function() { … }
});

注意下面的這個範例使用了ES7 property initializers,它使用了定義類別中static(靜態)屬性的方式:

// ES6+語法方式
class Video extends React.Component {
  static defaultProps = {
    autoPlay: false,
    maxLoops: 10,
  }
  static propTypes = {
    autoPlay: React.PropTypes.bool.isRequired,
    maxLoops: React.PropTypes.number.isRequired,
    posterFrameSrc: React.PropTypes.string.isRequired,
    videoSrc: React.PropTypes.string.isRequired,
  }
  render() { … }
}

下面這個是ES6的定義法,它是定義在外面,這個方式也很常見:

// ES6+語法方式
class Video extends React.Component {
  constructor(props) {
    super(props);
  }
  render() { … }
}

Video.propTypes = {

};

Video.defaultProps = {

};

State初始值

getInitialState定義state的初始值:

// ES5語法方式
var Video = React.createClass({
    getInitialState: function() {
        return {
            loopsRemaining: this.props.maxLoops,
        };
    },
})

不需要用getter方法,而有兩種方式可以定義,一種是在類別成員裡,一種是定義在建構子中:

//ES6+語法方式
class Video extends React.Component {
    state = {
        loopsRemaining: this.props.maxLoops,
    }
}

定義在建構子中感覺會比較清楚區分:

//ES6+語法方式
class Video extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        loopsRemaining: this.props.maxLoops,
    };
  }
}

Autobinding(自動綁定) “this”

React.createClass方法可以將元件內的函式,Autobinding(自動綁定)this的能力:

//ES5語法方式
var Contacts = React.createClass({
  handleClick: function(e) {
    console.log(this); 
  },
  render: function() { … }
});

變為元件後,你需要對每個會用到的方法,在建構子中用bind自己綁定this

//ES6+語法方式
class Contacts extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(e) {
    console.log(this); 
  }

  render() { … }
}

警告: 雖然你也可以直接在render裡直接用bind來綁定this,但這個作法是不建議使用的。在建構子中綁定才是正確的作法。參考eslint: react/jsx-no-bind

箭頭函式(arrow function)也有綁定this的能力,容易用的很,所以這也很常見:

//ES6+語法方式
class Contacts extends React.Component {
  handleClick = (e) => {
    console.log(this); // React Component instance
  }

  render() { … }
}

註:箭頭函式能綁定的語法很早就在React v0.13.0 Beta 1

參考資料