Travel in Time.

一些 React 元件設定與事件綁定的技巧

Published on
modern-react-with-redux

第六章談到 class component 有生命週期,即繼承React.Component 可借用的方法,除了 render 之外,其他可以自行決定是否要實作,將會在某些特定時機點被 React 觸發。

因為之前已經寫過 [React:認識生命週期方法及觸發時間點](../../Lidemy/React Lifecycle:認識生命週期方法及觸發時間點),這邊會就同樣的部分略過,以紀錄 CH6、CH7 中學到的實作知識為主。


不在 constructor 載入資料

在一開始練習時,將獲取用戶地理位置的程式碼寫在了 contructor 裡,但更好的方式,是讓這些獲得資料的操作放在 componentDidMount 統一管理,constructor 只用來設置初始化 state。

  • componentDidMount 第一次渲染到畫面,設定初始 data


初始化狀態的另一寫法

state 是一個 Javascript 物件,為 class component 管理狀態的地方,前面說過因為是繼承,要先實作 constructor 並呼叫 super() 引用父類別設置,不然無法使用 this 變數及初始化 this.state,不過,其實有另一種簡單的方式可以設定 state。

就是直接寫

不需要寫 constructor,卻相當於在 constructor 初始化 this.state

class App extends React.Component {
  state = { lat: null, errorMessage: ''}

  render() {
    return (
      <div>Latitude:</div>
    )
  }
}

用 create react app 建立,已經引用了 @babel/plugin-proposal-class-properties 套件,可以使用這個寫法,因為 babel 自動幫我們實作 constructor,不必再自己定義構造函數、呼叫 super 及處理 props


設定 default props

應該滿常在程式碼中看到這種寫法

<div className="loader">{props.message || 'Loading...'}</div>

課程介紹除了用 || 設置預設值,React 其實有為 component 提供「defaultProps」屬性,能使用它來為 props 添加預設值。

當父元件如果沒有傳對應的 props 進來,自動帶入設定預設的屬性值,不管是 functional 或 class component 都可以加上。

const Spinner = props => {
  return (
    <div>
      <div className="loader">{props.message}</div>
    </div>
  )
}

Spinner.defaultProps = {
  message: 'Loading...'
}

自行設定 renderbody

不是什麼技巧,只是增進程式碼的可讀性。

假設今天要渲染的東西設有多重條件、或判斷多,這種情況下不要把邏輯部份摻在 return 裡面,寫成一個 function,渲染時再呼叫。

舉例,我有一段 JSX,是根據 page 來決定顯示,以往習慣用三元運算子或 && 進行判斷:

// 按照 currentTab.page 的值渲染對應頁面
return (
  <div>
    <Tabs tabs={tabs} index={currentTab.index} />
    <>
      {currentTab.page === SERVICE_PAGE.CONTENT && <ServiceContent />}
      {currentTab.page === SERVICE_PAGE.RULES && <Rules />}
      {currentTab.page === SERVICE_PAGE.MY_PACKAGE && <MyPackage />}
    </>
  </div>
)

不過,其實能拉出來寫成 function,讓 currentTab.page 以參數傳入,讓整體邏輯拉出來,不會都放在 return 判斷

renderContent = (page) => {
  switch (page) {
    case SERVICE_PAGE.CONTENT:
      return <ServiceContent />
    case SERVICE_PAGE.RULES:
      return <Rules />
    case SERVICE_PAGE.MY_PACKAGE:
      return <MyPackage />
    default: 
      return <></>
  }
}

// 好像清爽許多?
render() {
  return (
    <div>
      <Tabs tabs={tabs} index={currentTab.index} />
      {renderContent(currentTab.page)}
    </div>
  )
}

畫面應該由 React 來控制

在 React 裡,元件有 controlled 和 uncontrolled 區別,前者透過 state 來保存資料、利用 setState 設定值,後者則是沒有綁定 state,像傳統作法由 DOM 本身處理。

以程式碼為例,input 元素就是 uncontrolled 的元件

class SearchBar extends React.Component {
  render() {
    return (
      <form>
        <label>Search</label>
        <input type="text" />
      </form>
    )
  }
}

它的值是由 HTML 元素內部管理和存放,當今天我們想在 React 上知道現在的 search value 是多少,不得不透過其他方式到 DOM 上面找值,因為並不存放在 React 中。

uncontrolled 看著簡單,然而當你需要控制的 DOM 數量一多起來,手動操作的量就變得繁重, 而 controlled component 是由資料本身來更動畫面,遵循 React「單向資料流」的原則,所以才需要使用到 state,因為這關係了資料是否由 React 所控制


而要怎麼讓 input 的值被 React 所管理,就要進行事件綁定來監聽。

不同事件,會連結不同的屬性名稱,像是 onChange、onClick、onSubmit 等等,其實跟我們原本在寫 HTML 元素很像,就是要元素本身擁有這個事件才能設定,像 div 就不會有 submit 事件。

監聽 input 的 onChange 事件,從參數 event 可以拿到用戶剛剛鍵入的文字,把它存入 state,並設定 input 永遠顯示 state 內的資料,這樣一來就可以做到資料被 React 所管理。

class SearchBar extends React.Component {
  state = { term: '' }
  
  render() {
    return (
      <form>
        <label>Search</label>
        <input
          type="text"
          value={this.state.term} // 顯示 state 的值
          onChange={(e) => this.setState({ term: e.target.value })} // 變動就改 state
        />
      </form>
    )
  }
}

處理 class component 中的 this 問題

事件綁定時,如果操作很多會將 callback function 拉出來寫,通常分配給事件的回調函式會以命名 on 或 handle 開頭,例如 onInputChangehandleInputChange,其實就跟原生 JS 寫法事件綁定寫法一樣。

但是,卻遇到以下的問題?

Cannot read property 'state' of undefined

報錯了!問題就出在 this 上?

class SearchBar extends React.Component {
  state = { term: '' }
  
  onFormSubmit(e) {
    e.preventDefault()
    console.log(this.state.term) // 🔴 Cannot read property 'state' of undefined
  }
  
  render() {
    return (
      <form onSubmit={this.onFormSubmit}>
        <label>Search</label>
        <input
          type="text"
          value={this.state.term}
          onChange={(e) => this.setState({ term: e.target.value })}
        />
      </form>
    )
  }
}

在 onFormSubmit 函式裡面用到了 this,原本是想要引用 searchBar 類別,但這時的 this.state 印出來卻是 undefined?


在[克服 JavaScript 的奇怪部分](../../[筆記] 克服 JS 的奇怪部分/)中有提到,this 的值取決於函式位置及函式被呼叫的方式,一般情況都是指向 window,當函式是連結到物件的方法時(即作為物件的方法被呼叫),this 才會指向物件本身。

有三種方式可以解決:

  1. 在 constructor 設定,以 bind 方法綁好 this 的值
class SearchBar extends React.Component {
  constructor(props) {
    super(props)
    this.state = { term: '' }
    this.onFormSubmit = this.onFormSubmit.bind(this) // ✅ it works!
  }
  
  onFormSubmit(e) {
    e.preventDefault()
    console.log(this.state.term)
  }
  
  // ...
}
  1. 寫成箭頭函式(ES6 之後的功能)

使用箭頭函式會自動將 this 的值確認指向

class SearchBar extends React.Component {
  state = { term: '' }
  
  onFormSubmit = (e) => {  // ✅ this 指向 SearchBar
    e.preventDefault()
    console.log(this.state.term)
  }
  
  // ...
}
  1. 在 callback function 裡面使用箭頭函式
class SearchBar extends React.Component {
  state = { term: '' }
  
  onFormSubmit(e) {
    e.preventDefault()
    console.log(this.state.term)
  }
  render() {
    return ( // ✅ 確保 onFormSubmit 的 this 指向,作爲 this(SearchBar) 被呼叫
      <form onSubmit={(e) => this.onFormSubmit(e)}>
        <label>Search</label>
        <input
          type="text"
          value={this.state.term}
          onChange={(e) => this.setState({ term: e.target.value })}
        />
      </form>
    )
}

不過第三種問題比較大,每次渲染 searchBar 時都會創建不同的 callback function,如果這個 callback function 會作為 props 傳入更下層的元件,就會造成不必要的重複渲染,建議用第二種方式來綁定,避免效能問題。


參考資料