使用了將近一週的 react Hook,期間嘗試將項目中原有的class component
改造成Hook
,比較Hook
和class
的區別,得出一些個人的思考與見解。
什麼是Hook
react官網上面對 Hook 是這樣描述的。
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
Hook提供了react中函數式組件操作state,響應state的能力。
最簡單的todo
,一個函數式組件實現功能一個按鈕點擊增加計數,一個p標籤來同步顯示計數的更新。不用Hook的情況下我們需要依賴class component
來進行外部props的更新。
import React, { Component } from 'react';
function Demo({ num, addNum }) {
return (
<>
<p>{num}</p>
<button onClick={addNum}>點我增加</button>
</>
);
};
class UseDemo extends Component {
state = { num: 0 };
addNum = () => {
this.setState({ num: this.state.num + 1 });
};
render() {
return <Demo num={this.state.num} addNum={this.addNum} />;
}
}
使用Hook來進行對同樣的todo來進行改造。
import React, { useState } from 'react';
function Demo() {
const [num, setNum] = useState(0);
return (
<>
<p>{num}</p>
<button onClick={() => setNum(num + 1)}>點我增加</button>
</>
);
};
Hook提供了函數式組件類似於生命週期的方法
在Hook之前的設計中,函數式組件的更新由props變化決定,自行完成更新到視圖。我們先來不用Hook寫一個倒計時功能。
import React, { Component } from 'react';
function Demo({ num }) {
return <p>{num === 0 ? '倒計時結束' : num}</p>;
};
class UseDemo extends Component {
timer = null;
state = { num: 60 };
render() {
return <Demo num={this.state.num} />;
}
componentDidMount() {
this.timer = setInterval(() => {
if (this.state.num === 0) {
clearInterval(this.timer);
this.timer = null;
} else {
this.setState({ num: this.state.num - 1 });
}
}, 1000);
}
componentWillUnmount() {
if (this.timer) {
clearInterval(this.timer);
}
}
}
我們完成了一個60秒倒計時的功能。在 UseDemo didMount 的時候生成一個60秒的計時器。爲了防止用戶在倒計時結束前退出當前組件渲染,在componentWillUnmount
的時候,如果計時器還在計時,把它清空掉。
我們使用Hook+函數組件來完成相同的功能。
function useIntervalCountDown(countNum) {
const [num, setNum] = useState(countNum);
const [timer, setTimer] = useState(null);
useEffect(() => {
if (num === 0 && timer) {
clearInterval(timer);
}
if (!timer) {
let timeId = setInterval(() => {
setNum(num - 1);
}, 1000);
setTimer(timeId); 1000);
}
return () => {
if (timer) {
clearInterval(timer);
setTimer(null);
}
};
}, [num, timer]);
return num;
}
function Demo() {
const num = useIntervalCountDown(60);
return <p>{num === 0 ? '倒計時結束' : num}</p>;
};
我們使用useEffect
來完成componentDidMount
和componentWillUnmount
生命週期的模擬。useEffect
接收兩個參數,第一個參數爲函數,第二參數是一個數組。數組中存在的變量變化時,useEffect
會觸發第一個入參的函數。第一個入參函數可以設置一個返回的函數值,這個函數將在組件取消掛載的前執行(近乎相當於componentWillUnmount
)。
Hook本質上就是給函數式組件提供各種類組件的能力。讓你像寫class Component
一樣來寫functional Component
。但是通過上面兩個例子可以發現,Hook改造前後,代碼量並沒有減少多少,那麼我們到底爲什麼需要react Hook。
Hook解決了什麼問題
class component 邏輯複用不方便
舉一個最經常寫的後臺管理系統頁面的例子。如下圖:
圖中可以看到一個table
呈現各個詳情資料。然後最後一列是各種操作按鈕。這種模式的頁面一般會呈現在點擊左側菜單欄後出現,在一個後臺管理系統會出現很多次。事實上,這些頁面除了請求接口(url,入參)以及表格呈現(表格的標題,渲染邏輯)不同,其它有很多邏輯是相同的。比如:
- componentDidMount之後,請求表格的內容接口,設置到state。
- 翻頁,改變頁面尺寸,改變入參拉取請求列表。
- 請求列表前後,開啓表格loading。
在class component
模式下,如果想要複用這部分的邏輯,操作到組件內部的state
,只能使用繼承的方式。
import React, { Component } from 'react';
// 僅僅舉例
export class BaseTableComponent extends Component {
// 拉取請求列表邏輯
fetchList = async () => {
const {
url,
params,
} = this.getRequestParams(); // 繼承子類自己內部實現
const { list, total } = await fetch(url, params);
this.setState({ tableList: list, total });
};
// 翻頁邏輯
handlePageChange = (current, size) => {
this.setState({ current, size }, () => {
this.fetchList();
});
};
// ... 省略其它組件複用的邏輯
componentDidMount() {
this.fetchList();
}
}
在上面簡單實現了一個抽象類BaseTableComponent
,在這個類中實現了拉取接口部分邏輯的抽取,列表翻頁邏輯的抽取。接下來寫頁面組件的時候,想要實現這部分邏輯都需要繼承這個類。
import { BaseTableComponent } from './BaseTableComponent';
export default class UserPage extends BaseTableComponent {
getRequestParams = () => {
return {
url: '/demo/',
params: { page: 1, size: 10 },
};
};
render() {
const { list, total, current, size } = this.state;
return (
<Table
dataSource={list}
pagination={{
total,
current,
size,
onChange: this.handlePageChange,
pagination: this.handlePageChange,
}}
/>
);
}
}
這樣子複用模式在實際項目中會帶來比較多的兩個問題:
-
複用邏輯組合複用不方便。
比如我所有頁面都用到了input
搜索請求頁面列表的邏輯,而單單頁面A,B沒有用到,我抽象出來的方法,在頁面AB組件中就存在冗餘。extends class
的繼承模式不能很好的解決這個問題。 -
複用邏輯必須一直關注父類用到的state。
因爲父類幫你抽象出來操作state的邏輯,因此,這部分佔用的state(比如list,total)在所有子類的方法中,都不能再使用了。隨着抽象的公用的邏輯越來越多,父類維護操作的state也會越來越多,需要關注不能使用的state也就越來越多。
Hook能很好的解決這個問題。函數式的組件和state
調用方法可以很方便的排列組合給需要的功能。
import React, { useState, useEffect } from 'react';
export function useFetch({ url, params }) {
const [list, setList] = useState([]);
const [total, setTotal] = useTotal(0);
useEffect(() => {
fetch(url, params)
.then(({ total, list }) => {
setList(list);
setTotal(total);
})
}, [params]);
return { list, total };
}
// 這裏爲了舉例簡單不引入 useCallback 等渲染更新優化的邏輯
// 頁面組件使用抽象的組件邏輯
export default () => {
const [current, setCurrent] = useState(1);
const [size, setSize] = useState(10);
// 引入請求列表邏輯
const { list, total } = useFetch({ url, params: { current, size } });
return (
<Table
dataSource={list}
pagination={{
total,
current,
size,
onChange: (current, size) => {
setCurrent(current);
setSize(size);
},
}}
/>
);
};
Hook給予了函數組件操作state,以及使用類似於class component
生命週期的能力。函數式組件本身高度靈活,可以拆卸複用各種小功能,而不會像class
一樣產生冗餘。
Hook實現邏輯的高聚合
回到開頭第二個計時器的例子。在使用class component
來實現計時器的時候,在componentDidMount
和componentWillUnmount
中分別進行了setInterval
和clearInterval
的操作。這就是class component
的第二個缺點,有時候我們實現一個功能,需要把邏輯分散在多個生命週期當中。當外部的prop會和內部同步更新時我們還要帶上getDerivedStateFromProps
的生命週期方法。使得組件在後期的維護上存在很重的負擔。接手代碼的同學需要貫穿整個react
數個生命週期方法才能明白你的一個數據處理邏輯。
在計時器的例子中,我們抽取了useIntervalCountDown
方法,把num, 和操作num的setNum邏輯放在一個函數裏面,貫穿在一起。無論是讀代碼邏輯的連貫性,還是代碼的聚合性都在一起,在維護度上的提升不是一星半點。
最後一點
其實Hook加入對react社區建設的意義也是非常積極的。Hook鼓勵你對數據以及操作數據的邏輯進行提取.既然你在日常工作中已經提取了不少邏輯,何不發佈到社區當中進行開源.實際上react Hook
發佈之後,react社區的其它核心組件包諸如react-router
,react-redux
都立即響應使用React Hook
進行了包的更新編寫。擁抱Hook
的速度足以證明react Hook
的積極意義。