在上一篇中介紹了 JSX Event 的使用方式,在本章節我們將近一步了解在 class component 中使用 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 onClick
時 this
變回預設繫結(Default binding)了,導致 handleClick
中拿不到 this.setState
的錯誤。
這個行為並不是 React 特有的,而是 JavaScript this
的特性。如果對於 this
沒有很清楚的讀者,可以查看這篇文章。
this
到 component instance 本身要解決掉 this
錯誤綁定的話可以使用一些技巧,包括:
bind
包裹 event handlerbind
以下就來分別介紹這些解法。
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 官方推薦的作法之一。
如果覺得需要在 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 官方推薦的做法之一。
最後一個解法是,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 上查看結果。從結果上我們也可以看到 handleClick
的 this
有正確綁定到 component instance 上了。
然而這個方法會有個小問題:每次執行 LoggingButton
的 render
時都會額外產生一次新的 () => this.handleClick()
function。這會造成 event handler 被作為更下層 component 的 props 傳入時,可能產生額外 re-render 的問題(當接收 event handler 的下層 component 為 PureComponent 時或者使用 React.memo 時)。更詳細的內容可以參考這篇文章。
因此比起 在 constructor 中用 bind
與 public class fields 語法,React 並不是這麼推薦此解法。
有時候在 JSX 的 list 之中,我們會想要把當前 iterate 到的元素資料傳進 event handler 中。在這狀況下可以用兩個方式達成目的:
data-id
props 加入 event 中第一種方法是利用 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:
接著還學到了可以用兩種方式把參數傳遞到 event handler 中: