iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 14
1
Modern Web

I Want To Know React系列 第 14

I Want To Know React - 正確使用 Event handler

上一篇中介紹了 JSX Event 的使用方式,在本章節我們將近一步了解在 class component 中使用 event handler 的常見問題、對應解法與技巧。

常見問題:Event Handler this 綁定錯誤

在 class component 中使用 event handler 常遇到的問題就是錯誤綁定 this,造成無法正確拿到 component instance 中的資料,舉例來說:

class LoggingButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};
  }

  handleClick(e) {
    console.log('this:', this);      // "this:" undefined
    this.setState(state => ({        // Uncaught TypeError: Cannot read property 'setState' of undefined
      isToggleOn: !state.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

讀者可以到 CodePen 中嘗看看結果。

從範例結果中可以看到,點擊 LoggingButton 時,handleClick 中的 this 的值會是 undefined 而沒有正確綁定到 component instance 上。

這是因為 this.handleClick 在賦值給 button 的 props onClickthis 變回預設繫結(Default binding)了,導致 handleClick 中拿不到 this.setState 的錯誤。

這個行為並不是 React 特有的,而是 JavaScript this 的特性。如果對於 this 沒有很清楚的讀者,可以查看這篇文章

正確綁定 this 到 component instance 本身

要解決掉 this 錯誤綁定的話可以使用一些技巧,包括:

  • 在 constructor 中用 bind 包裹 event handler
  • 使用實驗性的 public class fields 語法
  • 在 expression 中使用 arrow function 或 bind

以下就來分別介紹這些解法。

解法 1:在 constructor 中用 bind 包裹 event handler

第一種解法是在 constructor 中用 bind 將 event handler 的 this 綁定到 component instance 上 ,來確保 event handler 拿到的 this 是正確的。

如果對 JavaScript 的 bind 還不清楚的讀者可以閱讀這篇文章

接著讓我們來修改上一個範例吧:

class LoggingButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // This binding is necessary to make `this` work in the callback
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(e) {
    console.log('this:', this);  //  {props: {…}, context: {…}, refs: {…}, updater: {…}, state: {…}, …}
    this.setState(state => ({
      isToggleOn: !state.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

讀者也可以到 CodePen 上查看結果。

可以看到點擊下去後,handleClick 中的 this 有正確綁定到 component 的 instance 上了,this.setState 也因此可以正確使用了。

此解法是 React 官方推薦的作法之一。

解法 2:使用實驗性的 public class fields 語法

如果覺得需要在 contructor 中額外 bind event handler 很繁瑣的話,也可以考慮使用還在 ECMA Script 實驗階段的 public class fields 語法,這語法能讓 this語彙繫結的方式正確 bind 進 event handler 中。

用 public class fields 語法修改 LoggingButton 範例看看吧:

class LoggingButton extends React.Component {
	constructor(props) {
    super(props);
    this.state = {isToggleOn: true};
  }

  // This syntax ensures `this` is bound within handleClick.
  // Warning: this is *experimental* syntax.
  handleClick = (e) => {
    console.log('this:', this, e);
		this.setState(state => ({
      isToggleOn: !state.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

ReactDOM.render(<LoggingButton />, document.getElementById('root'));

讀者也可以到 CodePen 上試試看。

可以看到,點擊下去後,handleClick 中的 this 也有正確綁定到 component 的 instance 上。

要注意的是,如段落一開始所講,public class fields 語法還在實驗階段中,尚未成為正式的 JavaScript 語法,因此如果要使用此語法的話,需要安裝對應的 Babel plugin 才可使用。

另外,因為 Create React App 內建 public class fields,因此使用 Create React App 建立的專案可以直接使用這個解法。

此方法也是 React 官方推薦的做法之一。

解法 3:在 event expression 中用 arrow function / bind 包裹 event handler

最後一個解法是,JSX 的 expression 中直接使用 arrow function 或是 bind 直接綁定 event handler 的 this

Arrow function 中的 this 也是語彙繫結,詳細原理同樣請參考這一篇

就來嘗試在 LoggingButton 中使用這個解法吧:

class LoggingButton extends React.Component {
	constructor(props) {
    super(props);
    this.state = {isToggleOn: true};
  }

  handleClick(e) {
    console.log('this:', this, e);
		this.setState(state => ({
      isToggleOn: !state.isToggleOn
    }));
  }

  render() {
    // This syntax ensures `this` is bound within handleClick
    return (
      <button onClick={() => this.handleClick()}>
				{/* same as <button onClick={this.handleClick.bind(this)}> */}
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

有興趣的讀者可以到 CodePen 上查看結果。從結果上我們也可以看到 handleClickthis 有正確綁定到 component instance 上了。

然而這個方法會有個小問題:每次執行 LoggingButtonrender 時都會額外產生一次新的 () => this.handleClick() function。這會造成 event handler 被作為更下層 component 的 props 傳入時,可能產生額外 re-render 的問題(當接收 event handler 的下層 component 為 PureComponent 時或者使用 React.memo 時)。更詳細的內容可以參考這篇文章

因此比起 在 constructor 中用 bindpublic class fields 語法,React 並不是這麼推薦此解法。

技巧:傳遞參數到 event handler 中

有時候在 JSX 的 list 之中,我們會想要把當前 iterate 到的元素資料傳進 event handler 中。在這狀況下可以用兩個方式達成目的:

  1. 利用 Arrow function 或是 bind 包裹資料
  2. 利用 data-id props 加入 event 中

利用 arrow function / bind 包裹參數與 event handler

第一種方法是利用 arrow function 或者 bind 將目標參數與 event handler 包裹成一個新的函式註冊到 event 裡:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

讀者也可以到 CodePen 上試試實際範例。

在這種方式中,event handler 可以正確地拿到參數資訊。

然而缺點與上一個段落提到的一樣,每次執行 component 的 render 時都會產生新的 callback function,因此會造成下層的 PureComponent 也 re-render 的問題。

但好處是其語法十分簡單直觀,而除非下層 component 內容很多,否則 re-render 對效能的影響其實不會太大,因此也是 React 官方推薦使用的技巧

利用 data-id props 加入 element 中

第二種方法是在 element 上加上 data-id props 並戴上 id 的內容,如此一來在 this.deleteRow 中就可以透過 event.target.dataset.id 找到 data-id 的值了:

// in render function
<button data-id={id} onClick={this.deleteRow}>Delete Row</button>
// in event handler function
deleteRow = (e) => {
  const targetId = e.target.dataset.id;
  // delete row
};

有興趣的讀者可以到 CodePen 上試試實際範例。需要注意的一點事,data-id 這個 props 是以 kebab-case 的方式命名的,而沒有跟隨一般 props 的 camelCase 命名慣例。這算是 React props 的特例之一

這個方法好處是可以避開 PureComponent re-render 的問題。

但缺點是帶入的參數必須為 string。舉例來說,若 data-id 中的 value 為 JavaScript object,則 e.target.dataset.id 的內容會被轉成 string 變成 "[object Object]"。另外,此方法也較為不直觀,須透過 DOM 才可拿取到參數內容。

以上兩種方式都可以達到目的,就端看使用情境來決定要採納哪種方法即可。

小結

此章節中我們了解了可以使用以下方式來正確綁定 component 的 this 到 event handler:

  • 在 constructor 中用 bind 包裹 event handler
  • 使用實驗性的 public class fields 語法
  • 在 event expression 中用 arrow function / bind 包裹 event handler

接著還學到了可以用兩種方式把參數傳遞到 event handler 中:

  • 利用 arrow function / bind 包裹參數與 event handler
  • 將 data-id props 加入 element 中

參考資料


上一篇
I Want To Know React - 處理 Event
下一篇
I Want To Know React - 條件 render
系列文
I Want To Know React30

尚未有邦友留言

立即登入留言