前言
這篇文章主要介紹了React Hooks的一些實踐用法和場景,遵循我個人一貫的思(tao)路(是什麼-為什麼-怎麼做)
是什麼
Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.
簡單來說,上面這段官腔大概翻(xia)譯(shuo)就是告訴我們class能夠做到的老子用hooks基本可以做到,放棄抵抗吧,少年!
其實按照我自己的看法:React Hooks是在函式式元件中的一類以use為開頭命名的函式。 這類函式在React內部會被特殊對待,所以也稱為鉤子函式。
- 函式式元件
Hooks只能用於Function Component, 其實這麼說不嚴謹,我更喜歡的說法是建議只在於Function Component使用Hooks
- use開頭
React 約定,鉤子一律使用use字首命名,便於識別,這沒什麼可說的,要被特殊對待,就要服從一定的規則
- 特殊對待
Hooks作為鉤子,存在與每個元件相關聯的“儲存器單元”的內部列表。 它們只是我們可以放置一些資料的JavaScript物件。 當你像使用useState()一樣呼叫Hook時,它會讀取當前單元格(或在第一次渲染時初始化它),然後將指標移動到下一個單元格。 這是多個useState()呼叫每個get獨立本地狀態的方式
為什麼
解決為什麼要使用hooks的問題,我決定從hooks解決了class元件的哪些痛點和hooks更符合react的元件模型兩個方面講述。
1. class元件不香嗎?
class元件它香,但是暴露的問題也不少。Redux 的作者 Dan Abramov總結了幾個痛點:
- Huge components that are hard to refactor and test.
- Duplicated logic between different components and lifecycle methods.
- Complex patterns like render props and higher-order components.
第一點:難以重構和測試的巨大元件。 如果讓你在一個程式碼行數300+的元件里加一個新功能,你不慌嗎?你嘗試過註釋一行程式碼,結果就跑不了或者邏輯錯亂嗎?如果需要引入redux或者定時器等那就更慌了~~
第二點:不同元件和生命週期方法之間的邏輯重複。 這個難度不亞於蜀道難——難於上青天!當然對於簡單的邏輯可能通過HOC和render props來解決。但是這兩種解決辦法有兩個比較致命的缺點,就是模式複雜和巢狀。
第三點:複雜的模式,比如render props和 HOC。 不得不說我在學習render props的時候不禁發問只有在render屬性傳入函式才是render props嗎?好像我再任意屬性(如children)傳入函式也能實現一樣的效果; 一開始使用HOC的時候開啟React Develops Tools一看,Unknown是什麼玩意~看著一層層的巢狀,我也是無能為力。
以上這三點都可以通過Hooks來解決(瘋狂吹捧~)
2. hooks更符合React的程式設計模型?
我們知道,react強調單向資料流和資料驅動檢視,說白了就是元件和自上而下的資料流可以幫助我們將UI分割,像搭積木一樣實現頁面UI。這裡更加強調組合而不是巢狀,class並不能很完美地詮釋這個模型,但是hooks配合函式式元件卻可以!函式式元件的純UI性配合Hooks提供的狀態和副作用可以將元件隔離成邏輯可複用的獨立單元,邏輯分明的積木他不香嗎!
怎麼做
別問,問就是文件,如果不行的話,請熟讀並背誦文件...
但是(萬事萬物最怕But), 既然是實踐,就得假裝實踐過,下面就說說本人的簡單實踐和想法吧。
1. 轉變心智模型
我認為學習Hooks的主要成本不在於api的學習,而是在於心智模型的轉變~就像是當年react剛出時,jQuery盛行的時代,這也需要時間去理解這種基於virtual DOM的心智模型。出於本能,我們總喜歡在新事物的身上尋找舊事物的共同點,這種慣性思維應該批判性地對待(上升到哲學層面了,趕緊迴歸正題....),如果你在學習的過程中也有過把class元件的那套搬到Hooks,那麼恭喜你,你可能會陷入無限的wtf····, 下面舉幾個例子- state一把梭
// in class component
class Demo extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'Hello',
age: '18',
rest: {},
}
}
...
}
// in function component
function Demo(props) {
const initialState = {
name: 'Hello',
age: '18',
rest: {},
}
const [state, setState] = React.useState(initialState)
...
}
複製程式碼
- 嘗試模擬生命週期
// 這麼實現很粗糙,可以配合useRef和useCallback,但即使這樣也不完全等價於componentDidMount
function useDidMount(handler){
React.useEffect(()=>{
handler && handler()
}, [])
}
複製程式碼
- 在useEffect使用setInterval有時會事與願違
// count更新到1就不動了
function Counter() {
const [count, setCount] = React.useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
...
}
複製程式碼
其實,在class component環境下思考問題更像是在特定的時間點做特定的事情,例如我們會在constructor中初始化state,會在元件掛載後(DidMount)請求資料等,會在元件更新後(DidUpdate)處理狀態變化的邏輯,會在元件解除安裝前(willUnmount)清除一些副作用
然而在hooks+function component環境下思考問題應該更趨向於特定的功能邏輯,以功能為一個單元去思考問題會有一種豁然開朗的感覺。例如改變document的title、網路請求、定時器... 對於hooks,只是為了實現特定功能的工具而已
你會發現大部分你想實現的特定功能都是有副作用(effect)的,可以負責任的說useEffect是最干擾你心智模型的Hooks, 他的心智模型更接近於實現狀態同步,而不是響應生命週期事件。還有一個可能會影響你的就是每一次渲染都有它自己的資源,具體表現為以下幾點
- 每一次渲染都有它自己的Props 和 State:當我們更新狀態的時候,React會重新渲染元件。每一次渲染都能拿到獨立的狀態值,這個狀態值是函式中的一個常量(也就是會說,在任意一次渲染中,props和state是始終保持不變的)
- 每一次渲染都有它自己的事件處理函式:和props和state一樣,它們都屬於一次特定的渲染,即便是非同步處理函式也只能拿到那一次特定渲染的狀態值
- 每一個元件內的函式(包括事件處理函式,effects,定時器或者API呼叫等等)會捕獲某次渲染中定義的props和state(建議在分析問題時,將每次的渲染的props和state都常量化)
2. 所謂Hooks實踐
useState —— 相關的狀態放一起
- 不要所有state一把梭,可以寫多個useState,基本原則是相關的狀態放一起
- setXX的時候建議使用回撥的形式setXXX(xxx => xxx...)
- 管理複雜的狀態可以考慮使用useReducer(如狀態更新依賴於另一個狀態的值)
// 實現計數功能
const [count, setCount] = React.useState(0);
setCount(count => count + 1)
// 展示使用者資訊
const initialUser = {
name: 'Hello',
age: '18',
}
const [user, setUser] = React.useState(initialUser)
複製程式碼
useEffect —— 不接受欺騙的副作用
- 不要對依賴陣列撒謊,effect中用到的所有元件內的值都要包含在依賴中。這包括props,state,函式等元件內的任何東西
- 不要濫用依賴陣列項, 讓Effect自給自足
- 通過返回一個函式來清除副作用,在重新渲染後才會清除上一次的effects
// 修改上面count更新到1就不動了,方法1
function Counter() {
const [count, setCount] = React.useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
...
}
// 修改上面count更新到1就不動了,方法2( 與方法1的區別在哪裡 )
function Counter() {
const [count, setCount] = React.useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
...
}
複製程式碼
關於useEffect, 牆裂推薦Dan Abramov的A Complete Guide to useEffect,一篇支稱整篇文章架構的深度好文!
useReducer —— 強大的狀態管理機制
- 把元件內發生了什麼(actions)和狀態如何響應並更新分開表述,是Hooks的作弊模式
/** 修改需求:每秒不是加多少可以由使用者決定,可以看作不是+1,而是+step*/
// 方法1
function Counter() {
const [count, setCount] = React.useState(0);
const [step, setStep] = React.useState(1);
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
...
}
// 方法2( 與方法1的區別在哪裡 )
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { ...state, count: count + step };
} else if (action.type === 'step') {
return { ...state, step: action.step };
}
}
function Counter() {
const [state, dispatch] = React.useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
...
}
複製程式碼
useCallback —— FP裡使用函式的好搭檔
說這個之前,先說一說如果你要在FP裡面使用函式,你要先要思考有替代方案嗎?
方案1: 如果這個函式沒有使用元件內的任何值,把它提到元件外面去定義
方案2:如果這個函式只是在某個effect裡面用到,把它定義到effect裡面
如果沒有替代方案,就是useCallback出場的時候了。
- 返回一個 memoized 回撥, 不要對依賴陣列撒謊
// 場景1:依賴元件的query
function Search() {
const [query, setQuery] = React.useState('hello');
const getFetchUrl = React.useCallback(() => {
return `xxxx?query=${query}`;
}, [query]);
useEffect(() => {
const url = getFetchUrl();
}, [getFetchUrl]);
...
}
// 場景2:作為props
function Search() {
const [query, setQuery] = React.useState('hello');
const getFetchUrl = React.useCallback(() => {
return `xxxx?query=${query}`;
}, [query]);
return <MySearch getFetchUrl={getFetchUrl} />
}
function MySearch({ getFetchUrl }) {
useEffect(() => {
const url = getFetchUrl();
}, [getFetchUrl]);
...
}
複製程式碼
useRef —— 有記憶功能的可變容器
- 返回一個可變的 ref 容器物件,其 .current 屬性被初始化為傳入的引數(initialValue)。返回的 ref 物件在元件的整個生命週期內保持不變,也就是說會在每次渲染時返回同一個 ref 物件
- 當 ref 物件內容發生變化時,useRef 並不會通知你。變更 .current 屬性不會引發元件重新渲染
- 可以在ref.current 屬性中儲存一個可變值的“盒子“。常見使用場景:儲存指向真實DOM / 儲存事件監聽的控制程式碼 / 記錄Function Component在某次渲染的值( eg:上一次state/props,定時器id.... )
// 儲存不變的引用型別
const { current: stableArray } = React.useRef( [1, 2, 3] )
<Comp arr={stableArray} />
// 儲存dom引用
const inputEl = useRef(null);
<input ref={inputEl} type="text" />
// 儲存函式回撥
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}
複製程式碼
useMemo —— 記錄開銷大的值
- 返回一個 memoized 值,不要對依賴陣列撒謊
- 大多數時候可以優先考慮使用useRef,useMemo常來處理開銷較大的計算
- 可以依賴useMemo作為效能優化,但不能是語義保證(未來可能會忘記記憶值,比如為了釋放記憶體)
// 此栗子來自文件
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製程式碼
useContext —— 功能強大的上下文
- 接收一個 context (React.createContext 的返回值)並返回該 context 的當前值,當前的 context 值由上層元件中最先渲染的 <MyContext.Provider value={value}> 的 value決定
- 當元件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext provider 的 context value 值,如果重新呈現元件非常昂貴,那麼可以通過使用useMemo來優化它
// 此栗子來自文件
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
複製程式碼
彩蛋
說是彩蛋,其實是補充說明~~
1. 一條重要的規則(程式碼不規範,親人兩行淚)
hooks除了要以use開頭,還有一條很很很很重要的規則,就是hooks只允許在react函式的頂層被呼叫(這裡牆裂推薦Hooks必備神器eslint-plugin-react-hooks)
考慮到出於研(gang)究(jing)精神的你可能會問,為什麼不能這麼用,我偏要的話呢?如果我是hooks開發者,我會毫不猶豫地說出門右轉,有請下一位開發者!當然如果你想知道為什麼這麼約定地話,還是值得探討一下的。其實這個規則就是保證了元件內的所有hooks可以按照順序被呼叫。那麼為什麼順序這麼重要呢,不可以給每一個hooks加一個唯一的標識,這樣不就可以為所欲為了嗎?我以前一直都這麼想過直到Dan給了我答案,簡單點說就是為了hooks最大的閃光點——custom-hooks
2. custom-hooks
給我的感覺就是custom-hooks是一個真正詮釋了React的程式設計模型的組合的魅力。你可以不看好它,但它確實有過人之處,至少它呈現出思想讓我越想越上頭~~以至於vue3.0也借鑑了他的經驗,推出了Vue Hooks。反手推薦一下react conf 2018的custom-hooks。
// 修改頁面標題
function useDocumentTitle(title) {
useEffect (() => {
document.title = title;
}, [title]);
}
// 使用表單的input
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
return {
value,
onChange: handleChange
};
}
複製程式碼
寫在最後
最後丟擲兩個討論的小問題。
-
React Hooks沒有缺點嗎?
- 肯定是有的,給我最直觀的感受就是令人又愛又恨的閉包
- 不斷地重複渲染會帶來一定的效能問題,需要人為的優化
-
上面說了寫了很多的setInterval的程式碼,可以考慮封裝成一個custom-hooks?
- 可以考慮封裝成useInterva,關於封裝還是牆裂推薦Dan的 Making setInterval Declarative with React Hooks
- 如果有一堆特定的功能hooks,是不是完全可以通過組裝各種hooks完成業務邏輯的開發,例如網路請求、繫結事件監聽等
本人能力有限,如果有哪裡說得不對的地方,歡迎批評指正!
真的真的最後,怕你錯過,再次安利Dan Abramov的A Complete Guide to useEffect,一篇支稱整篇文章架構的深度好文!