關於為什麼使用React新特性Hook的一些實踐與淺見
前言
關於Hook的定義官方文件是這麼說的:
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
簡單來說,就是在使用函式式元件時能用上state,還有一些生命週期函式等其他的特性。
如果想了解Hook怎麼用,官方文件和阮一峰的React Hooks 入門教程都講得很清楚了,我建議直接看官方文件和阮大神的文章即可。
本篇部落格只講為什麼要用React的Hook新特性,以及它解決了什麼問題。
為什麼使用Hook?
讓我們先看看別人怎麼說。
阮大神的文章中給了一個示例程式碼:
import React, { Component } from "react"; export default class Button extends Component { constructor() { super(); this.state = { buttonText: "Click me, please" }; this.handleClick = this.handleClick.bind(this); } handleClick() { this.setState(() => { return { buttonText: "Thanks, been clicked!" }; }); } render() { const { buttonText } = this.state; return <button onClick={this.handleClick}>{buttonText}</button>; } }
並且提出:
這個元件類僅僅是一個按鈕,但可以看到,它的程式碼已經很"重"了。
真實的 React App 由多個類按照層級,一層層構成,複雜度成倍增長。
再加入 Redux,就變得更復雜。
實際上,上面這個程式碼的“重”有部分來源於寫法問題,他可能並沒有“重”,讓我們看看下面這種class寫法:
import React, { Component } from "react"; export default class Button extends Component { state = { buttonText: "Click me, please" } handleClick = () => { this.setState(() => { return { buttonText: "Thanks, been clicked!" }; }); } render() { const { buttonText } = this.state; return <button onClick={this.handleClick}>{buttonText}</button>; } }
然後再對比下使用了Hook的函式式元件:
import React, { useState } from "react"; export default function Button() { const [buttonText, setButtonText] = useState("Click me, please"); function handleClick() { return setButtonText("Thanks, been clicked!"); } return <button onClick={handleClick}>{buttonText}</button>; }
即使是我們簡化過的class寫法,比起Hook的看起來好像也確實“重”了點。
Hook的語法確實簡練了一些,但是這個理由並不是那麼充分。
阮大神同時列舉了Redux 的作者 Dan Abramov 總結了元件類的幾個缺點:
- 大型元件很難拆分和重構,也很難測試。
- 業務邏輯分散在元件的各個方法之中,導致重複邏輯或關聯邏輯。(這裡我認為阮大神寫的可能有點問題,應該是是各個生命週期方法更為準確)
- 元件類引入了複雜的程式設計模式,比如 render props 和高階元件。
這三點都是事實,於是有了函式化的元件,但之前的函式化元件沒有state和生命週期,有了Hook那麼就可以解決這個痛點。
而且Hook並不只是這麼簡單,通過自定義Hook,我們可以將原有元件的邏輯提取出來實現複用。
用useEffect解決生命週期導致的重複邏輯或關聯邏輯
上面舉的幾個缺點,第一點和第三點你可能很容易理解,第二點就不容易理解了,所以我們需要深入到具體的程式碼中去理解這句話。
我們看看下面這段程式碼:
import React, { Component } from "react";
export default class Match extends Component {
state={
matchInfo:''
}
componentDidMount() {
this.getMatchInfo(this.props.matchId)
}
componentDidUpdate(prevProps) {
if (prevProps.matchId !== this.props.matchId) {
this.getMatchInfo(this.props.matchId)
}
}
getMatchInfo = (matchId) => {
// 請求後臺介面獲取賽事資訊
// ...
this.setState({
matchInfo:serverResult // serverResult是後臺介面的返回值
})
}
render() {
const { matchInfo } = this.state
return <div>{matchInfo}</div>;
}
}
這樣的程式碼在我們的業務中經常會出現,通過修改傳入賽事元件的ID,去改變這個賽事元件的資訊。
在上面的程式碼中,受生命週期影響,我們需要在載入完畢和Id更新時都寫上重複的邏輯和關聯邏輯。
所以現在你應該比較好理解這句話:業務邏輯分散在元件的各個生命週期方法之中,導致重複邏輯或關聯邏輯。
為了解決這一點,React提供了useEffect這個鉤子。
但是在講這個之前,我們需要先了解到React帶來的一個新的思想:同步。
我們在上面的程式碼中所做的實際上就是在把元件內的狀態和元件外的狀態進行同步。
所以在使用Hook之前,我們需要先摒棄生命週期的思想,而用同步的思想去思考這個問題。
現在再讓我們看看改造後的程式碼:
import React, { Component } from "react";
export default function Match({matchId}) {
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 請求後臺介面獲取賽事資訊
// ...
setMatchInfo(serverResult) // serverResult是後臺介面的返回值
}, [matchId])
return <div>{matchInfo}</div>;
}
看到這個程式碼,再對比上面的程式碼,你心中第一反應應該就是:簡單。
React.useEffect接受兩個引數,第一個引數是Effect函式,第二個引數是一個數組。
元件載入的時候,執行Effect函式。
元件更新會去判斷陣列中的各個值是否變動,如果不變,那麼不會執行Effect函式。
而如果不傳第二個引數,那麼無論載入還是更新,都會執行Effect函式。
順便提一句,這裡有元件載入和更新的生命週期的概念了,那麼也應該是有元件解除安裝的概念的:
import React, { Component } from "react";
export default function Match({matchId}) {
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 請求後臺介面獲取賽事資訊
// ...
setMatchInfo(serverResult) // serverResult是後臺介面的返回值
return ()=>{
// 元件解除安裝後的執行程式碼
}
}, [matchId])
return <div>{matchInfo}</div>;
}
}
這個常用於事件繫結解綁之類的。
用自定義Hook解決高階元件
React的高階元件是用來提煉重複邏輯的元件工廠,簡單一點來說就是個函式,輸入引數為元件A,輸出的是帶有某邏輯的元件A+。
回想一下上面的Match元件,假如這個元件是頁面A的首頁頭部用來展示賽事資訊,然後現在頁面B的側邊欄也需要展示賽事資訊。
問題就在於頁面A的這塊UI需要用div,而頁面B側邊欄的這塊UI需要用到span。
保證今天早點下班的做法是複製A頁面的程式碼到頁面B,然後改下render的UI即可。
保證以後早點下班的做法是使用高階元件,請看下面的程式碼:
import React from "react";
function hocMatch(Component) {
return class Match React.Component {
componentDidMount() {
this.getMatchInfo(this.props.matchId)
}
componentDidUpdate(prevProps) {
if (prevProps.matchId !== this.props.matchId) {
this.getMatchInfo(this.props.matchId)
}
}
getMatchInfo = (matchId) => {
// 請求後臺介面獲取賽事資訊
}
render () {
return (
<Component {...this.props} />
)
}
}
}
const MatchDiv=hocMatch(DivUIComponent)
const MatchSpan=hocMatch(SpanUIComponent)
<MatchDiv matchId={1} matchInfo={matchInfo} />
<MatchSpan matchId={1} matchInfo={matchInfo} />
但是實際上有的時候我們的高階元件可能會更復雜,比如react-redux的connect,這就是高階元件的複雜化使用方式。
又比如:
hocPage(
hocMatch(
hocDiv(DivComponent)
)
)
毫無疑問高階元件能讓我們複用很多邏輯,但是過於複雜的高階元件會讓之後的維護者望而卻步。
而Hook的玩法是使用自定義Hook去提煉這些邏輯,首先看看我們之前使用了Hook的函式式元件:
import React, { Component } from "react";
export default function Match({matchId}) {
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 請求後臺介面獲取賽事資訊
// ...
setMatchInfo(serverResult) // serverResult是後臺介面的返回值
}, [matchId])
return <div>{matchInfo}</div>;
}
然後,自定義Hook:
function useMatch(matchId){
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 請求後臺介面獲取賽事資訊
// ...
setMatchInfo(serverResult) // serverResult是後臺介面的返回值
}, [matchId])
return [matchInfo]
}
接下來,修改原來的Match元件
export default function Match({matchId}) {
const [matchInfo]=useMatch(matchId)
return <div>{matchInfo}</div>;
}
相比高階元件,自定義Hook更加簡單,也更加容易理解。
現在我們再來處理以下這種情況:
hocPage(
hocMatch(
hocDiv(DivComponent)
)
)
我們的程式碼將不會出現這種不斷巢狀情況,而是會變成下面這種:
export default function PageA({matchId}) {
const [pageInfo]=usePage(pageId)
const [matchInfo]=useMatch(matchId)
const [divInfo]=useDiv(divId)
return <ul>
<li>{pageInfo}</li>
<li>{matchInfo}</li>
<li>{divInfo}</li>
</ul>
}
是否需要改造舊的class元件?
現在我們瞭解到了Hook的好,所以就需要去改造舊的class元件。
官方推薦不需要專門為了hook去改造class元件,並且保證將繼續更新class相關功能。
實際上我們也沒有必要專門去改造舊專案中的class元件,因為工作量並不小。
但是我們完全可以在新的專案或者新的元件中去使用它。
總結
Hook是對函式式元件的一次增強,使得函式式元件可以做到class元件的state和生命週期。
Hook的語法更加簡練易懂,消除了class的生命週期方法導致的重複邏輯程式碼,解決了高階元件難以理解和使用困難的問題。
然而Hook並沒有讓函式式元件能做到class元件做不到的事情,它只是讓很多事情變得更加簡單而已。
class元件並不會消失,但hook化的函式式元件將是趨勢。