一些 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>
)
}
}
this
問題
處理 class component 中的 事件綁定時,如果操作很多會將 callback function 拉出來寫,通常分配給事件的回調函式會以命名 on 或 handle 開頭,例如 onInputChange
或 handleInputChange
,其實就跟原生 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 才會指向物件本身。
有三種方式可以解決:
- 在 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)
}
// ...
}
- 寫成箭頭函式(ES6 之後的功能)
使用箭頭函式會自動將 this 的值確認指向
class SearchBar extends React.Component {
state = { term: '' }
onFormSubmit = (e) => {
// ✅ this 指向 SearchBar
e.preventDefault()
console.log(this.state.term)
}
// ...
}
- 在 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 傳入更下層的元件,就會造成不必要的重複渲染,建議用第二種方式來綁定,避免效能問題。
參考資料