在 React 16 中,除去 Fiber 架構外,Hooks 是最使人激動的一個特性,相比於 class component,Hooks 加持後的 function component 在寫法與思路上都大有不一樣,不少時候顯得更爲簡潔與清爽(熵更低,弱化生命週期的概念),同時解決了使人煩惱的 this 指針指向問題,仍是很香的。react
但理性來講,到目前爲止,hooks 仍是一個坑不少的階段,而且也缺少一個成體系的最佳實踐,如下談談我對 hooks 的一些淺薄的認識。git
那麼,是時候發車了。github
咱們能夠把這裏的 state 看作咱們在 class component 中使用的 this.state。ajax
咱們的每一次 setState 操做,在改變了值以後,都會引起 rerender 操做,從而觸發頁面的更新(可是若是沒有改變的話,則不會觸發 rerender,咱們在後面將會利用這一特性作一件有趣的事情)。編程
同時,setState 能夠以函數做爲參數,這個時候咱們能夠獲取到最新的 state 值(在第一個回調參數)。redux
import React, { useState } from 'react';
function App() {
const [ state, setState ] = useState(0);
return (
<span>{state}</span>
)
}
複製代碼
useEffect能夠說是全部 hooks API 中最像是聲明週期的鉤子了,很容易讓人理解成爲,若是依賴數組爲空,那麼它等價爲 componentDidMount,可是真的這樣嗎?數組
咱們能夠這樣去理解咱們的函數組件,函數組件的每次運行都至關於 class component 中的一次 render,每輪都會保留它的閉包,因此,咱們的 useEffect 實際保留了它運行輪次的 state 和 props 狀態(若是依賴不更新,那麼狀態不更新),這也就是 useEffect 和 componentDidMount 生命週期的關係。緩存
import React, { useEffect } from 'react';
function App() {
useEffect(() => {
console.log('I am mount');
return () => {
console.log('before next run, I am cleaned');
}
}, []);
複製代碼
useLayoutEffect 與 useEffect 的不一樣在於,useLayoutEffect 會在 DOM 渲染以前執行,而 useEffect 會在 DOM 渲染以後執行,因此咱們能夠利用這個特性,避免一些因爲 DOM 渲染以後進行操做致使的白屏問題。性能優化
useCallback 能夠幫助咱們緩存函數(useMemo一樣能夠作到,寫法不一樣),經過手動控制依賴,作到減小由於函數的更新致使子組件的更新(帶來的性能問題很是明顯)antd
import React, { useCallback } from 'react';
function App() {
const cb = useCallback(() => { console.log('callback') }, []);
return (
<button onClick={cb}></button>
)
}
複製代碼
useMemo 能夠爲咱們的 function component 提供緩存的能力,在一些重計算的場景下,能夠減小重複計算的次數,起到明顯的性能提高。 固然,useMemo一樣能夠用來緩存組件,起到相似與 class component 中 shouldComponentUpdate 的做用,讓咱們手動經過管理依賴的方式作到控制子組件的更新(固然這個手動管理的成本是很是高的)
由於在 hooks 中,咱們所聲明的全部變量是隻屬於它的閉包的,因此,咱們沒法作到變量的一個共享。由於 immutable 的 state 並不適合咱們存儲一些不參與 UI 顯示的變量。hooks 爲咱們提供了 useRef 去存儲 mutable 的不參與 UI 顯示的變量,而且能夠在每一輪 render 中共享。
useRef 不只能夠用來存儲對 Dom 元素的引用(它的本意),更能夠用來存儲咱們須要在每輪 render 中共享的 mutable 的變量(可能很是經常使用)。
import React, { useRef } from 'react';
function App() {
const td = useRef(1);
console.log(td.current); // 1
...
複製代碼
在當前版本中的 useReducer 事實上是對 useState 的一層封裝,實現了 redux 的一套原理(以前的版本是 useState 是對 useReducer 的一層封裝)
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
複製代碼
假定咱們已經有了一個 Context ,而且咱們的子組件已經在 Provider 包裹下,咱們能夠直接使用 useContext 去獲取值,而非使用回調去獲取值。 同時,咱們也能夠對某些 Context 進行 useContext 的封裝,讓咱們能夠在不一樣的組件中方便的使用 Context 中的數據。
// 假定咱們已經有 Context
function Child(props) {
const { value } = useContext(Context);
return (
<div> {value} </div>
)
}
複製代碼
咱們能夠將 Context 、useReducer 與 useContext 結合起來,打造咱們本身的 Redux
const CTX = React.createContext(null);
const reducer = (state, action) => {
switch(action.type) {
default:
reutrn state;
}
}
const Context = function({ children }) {
const [state, dispatch] = useReducer(reducer, {});
return (
<CTX.Provider value={ state, dispatch }> {children} </CTX.Provider> ) } 複製代碼
咱們應該樹立一個理念,在 function component 中,全部的狀態,都是隸屬於它的閉包的,因此致使了 咱們每一輪的 Render 都會有本身的一個閉包,全部的 useEffect 與 useLayoutEffect 都在其最後一次更新的閉包中 Hooks處理請求
hooks 編程有些相似於響應式編程,同時,爲了能夠老是拿到最正確的值,正確的去書寫 hooks依賴 是很是重要的,也就是所謂的對依賴誠實,這樣才能保證咱們最終發送請求之時,能夠取到正確的 state 和 props。
爲了可以正確的處理請求,有一種想法是——將請求的函數放置於 useEffect 中,這樣子就能夠確保咱們每時每刻都會去正確的處理其中的依賴問題。 處理競態 咱們知道,在 hooks 裏,每一次 Render 以及 每一次 useEffect 的執行都是在它本身所處輪次的閉包中,因此,咱們處理競態的一個思路就來源於這裏。
咱們的依賴變化會觸發咱們的 ajax 操做,因此當第二次請求發生時,實際上上一次 effect 已經到了清理反作用時期,因此執行了 return 中的函數,將咱們的flag置爲true,這樣,當咱們的請求返回之時,其effect 所在的閉包是能夠感知到執行結束的狀態的,從而拋棄舊值,達到對競態的正確處理。
useEffect(() => {
let flag = false;
ajax().then(res => {
if (!flag) {
//...do something
}
})
return () => {
flag = true;
}
}, [deps])
複製代碼
咱們能夠將請求函數用普通函數的方法,放置於整個 function 中,這樣足以確保咱們這個函數可以拿到當前 render 輪次所依賴的 state 和 props,若是有性能方面的顧慮,能夠考慮使用 useCallback 去進行包裝(但此時必定要對依賴誠實)
function App() {
const [flag, setFlag] = useState(0);
const ajax = () => {
_ajax(props)
};
useEffect(() => {
ajax();
}, [flag]);
return (
...
)
}
複製代碼
這個時候必定要注意的一點是,咱們的觸發 flag,必定要在最後修改(先進行預操做——其它的 state 修改),肯定咱們的 effect 更新時,索引用的,是最新的 ajax 請求函數。
非渲染參數使用 ref 進行保存 由於咱們在 effect 中,永遠能夠正確的獲取到 ref 值,因此,當咱們的參數不參與渲染時,咱們能夠用 useRef 生成的 ref 對其進行管理,這樣咱們就能夠不用去擔憂因爲 ref 所引用參數的變化問題(同時,也不會觸發頁面的 rerender)
const name = useRef('小明')
const ajax = useCallback(() => {
ajax({ name })
}, []);
// 修改 param 直接操做 ref
name.current = '123';
複製代碼
利用 setState 的回調處理獲取 state 的問題 由於
const [state, setState] = useState(0);
// 利用 setState 的回調拿到最新的 state,返回原值,能夠不觸發 rerender(極端狀況下能夠用於性能優化)
const update = useCallback(() => {
setState(state => {
// 作你想作的任何事情
return state;
})
}, []);
複製代碼
試想一個很騷的場景,若是咱們使用 setter 嵌套(而且都返回原始值),那麼咱們是否是能夠在無任何依賴狀況下用 state 作任何想作的事情呢(代碼可讀性忽略)
const trigger = useCallback(() => {
setState1(state1 => {
setState2(state2 => {
console.log(state1 + state2);
return state2;
})
return state1;
})
});
複製代碼
利用 useReducer 和 setState 結合處理獲取 state 和 props 的問題 由於上面的方法,咱們只能確保咱們能夠無依賴的拿到 state ,可是咱們卻不能在無依賴的狀況下拿到 props 那麼咱們能夠怎麼辦呢。 咱們可能把 useReducer 的 reducer 放在 function component 函數體內,利用 dispatch 最終觸發的是最新的閉包中的 reducer 來確保咱們能夠拿處處於最新狀態的 props
function App({ a, b, c }) {
const reducer = (state, action) => {
switch(action.type) {
case 'init':
// 這裏永遠能夠拿到最新的 a
return Object.assign(state, { a: a });
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, {});
return (
<div>{ state.a }</div>
)
}
複製代碼
咱們能夠作相似於 class component 中的 PureComponent 這樣的操做,咱們能夠用 React.memo 包裹大部分的組件(會帶來額外的比較,性能不必定是最佳的)
利用 React.memo,咱們能夠作到讓 React 對咱們的組件進行淺比較,
const Child = function({ a, b, c }) {
return <div>{a}{b}{c}</div>
}
export default React.memo(Child);
複製代碼
function App({ a, b, c }) {
const RenderComponent = useMemo(() => {
return <div>{c}</div>
}, [c]);
return (
<RenderComponent /> ) } 複製代碼
在這裏,可使用我上面所介紹的 trick ,減小依賴的數量,從而減小 rerender 的次數 Eg: trigger.jsx 開關
const useTrigger = () => {
const [state, setState] = useState(false);
const trigger = useCallback(() => {
setState(ste => !ste);
}, []);
return { state, trigger };
}
// vs
const useTrigger = () => {
const [state, setState] = useState(false);
const trigger = useCallback(() => {
setState(ste => !ste);
}, [state]);
return { state, trigger };
}
複製代碼
const useInput = () => {
const [value, setValue] = useState('');
const onChange = val => {
setValue(val.target.value);
};
return {
value,
onChange
};
};
複製代碼
export const useSubmit = submitFunction => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [res, setRes] = useState(null);
const trigger = useCallback(() => {
try {
if (_.isFunction(submitFunction)) {
(async () => {
let res = await submitFunction();
if (res) {
setRes(res);
}
})();
}
} catch (e) {
setError(e);
} finally {
setLoading(true);
}
}, [submitFunction]);
return [loading, res, error];
};
複製代碼
不少時候,咱們都會依賴於 props 去計算咱們的 state,在 class component 中給咱們提供了 getDerivedStateFromProps 生命週期供咱們去作相似的操做,可是在 hooks 裏,咱們並無這樣的生命週期的概念,那咱們應該如何去作呢?
咱們能夠利用 useMemo 去進行對 props 的計算操做,經過正確處理依賴,就能夠籍由 useMemo 的記憶特性,讓咱們以最小的成本去正確的更新 state (高成本的方案是每一次去計算將值賦給閉包中的普通變量)。
import React, { useMemo } from 'react';
function App({ data }) {
// 只有 data 更新時從新計算
const info = useMemo(() => {
// 對 data 進行一系列的計算操做
return newData;
}, [data]);
}
複製代碼
以前所說的大都是利用 hooks 去處理邏輯問題,那麼 hooks 是否能夠像是高階組件那樣,爲咱們返回一個組件呢,答案是能夠的,而且利用這樣的能力,咱們還能夠簡化不少狀況下咱們的編程。
import React, { useState, useCallback } from 'react';
import { Modal } from 'antd';
export default function useModal() {
const [show, setShow] = useState<boolean>(false);
const openModal = useCallback(() => {
setShow(true);
}, []);
const closeModal = useCallback(() => {
setShow(false);
}, []);
const CusModal: React.SFC = ({ children, ...props }) => {
return (
<Modal visible={show} {...props}> {children} </Modal>
)
}
return {
show,
setShow,
openModal,
closeModal,
CusModal
}
}
複製代碼
利用 ref hooks 進行一些無侵入操做(react 官方 不推薦) 由於 ref 能夠拿到原始 dom,咱們能夠利用這個特性作一些操做,例如說侵入代碼性的埋點遷移至 ref(減小對原始代碼侵入)
eg:利用 ref 記錄停留時間(能夠作無侵入埋點)
export const useHoverTime = eventName => {
const EV = `${ eventName}`;
const ref = useRef(null);
useEffect(() => {
localStorage.setItem(EV, 0);
return () => {
const time = localStorage.getItem(EV);
// do something
localStorage.setItem(EV, null);
};
}, []);
useEffect(() => {
let startTime = null;
let endTime = null;
const overHandler = () => {
startTime = new Date();
};
const outHandler = () => {
endTime = new Date();
localStorage.setItem(
EV,
parseInt(localStorage.getItem(EV)) +
parseInt(endTime - startTime)
);
startTime = 0;
endTime = 0;
};
if (ref.current) {
ref.current.addEventListener('mouseover', overHandler);
ref.current.addEventListener('mouseout', outHandler);
}
return () => {
if (ref.current) {
ref.current.removeEventListener('mouseover', overHandler);
ref.current.removeEventListener('mouseout', outHandler);
}
};
}, [ref]);
return ref;
};
複製代碼
React-hook-form 利用 ref 進行的表單的註冊和提交攔截(我的認爲也是一種很是清奇的思路)
immutable.js 的使用複雜度是很是高的,可是有時候咱們又但願咱們的 React App 性能更好,節省沒必要要的 rerender,那麼 Immer.js 就是一個很是好的選擇(事實上dva也使用了immer做爲底層庫)
咱們能夠在使用 useReducer 的時候,使用 Immer 進行狀態的變動,從而使得咱們最新的 state 是 immutable 的。
const reducer = (state, action) => {
switch (action.type) {
case 'initData':
return produce(state, draft => {
draft.data = action.data;
});
複製代碼
從何種角度看,useReducer + useContext + Context 的組合都在作傳統 Redux 所在作的事情,那麼,有沒有可能讓咱們的原生 hooks 使用上 Redux 的中間件呢(本質上劫持了 action ,與 Redux 的 Api 無關)?! 是能夠的,事實上,這裏至關於把 Redux 中間件的實現遷移到了 hooks 上,咱們固然能夠本身實現,可是 react-use 這個庫裏幫咱們作了集成,咱們能夠方便的直接使用它。
// 建立加強了中間件的 reducer , 這裏的例子增長了 redux-logger 與 redux-thunk
const useLoggerReducer = createReducer(logger, thunk);
export default function App() {
const [state, dispatch] = useLoggerReducer(reducer, initState);
複製代碼
這樣子,咱們即可以利用 redux-thunk、redux-saga 等中間件進行異步任務的處理,使用 redux-logger 進行 action 的打印和先後 state 的 diff。
const combineReducers = (reducers) => {
const keys = Object.keys(reducers);
const initObj = {};
keys.forEach(key => {
let draftState = reducers[key](undefined, { type: '' });
if (!draftState) {
draftState = {};
console.warn(
`[Error]: 在combineReducers 中 Reducer 須要初始化!`
);
}
initObj[key] = draftState;
})
return (state, action) => {
keys.forEach(key => {
const prevState = initObj[key];
initObj[key] = reducers[key](prevState, action);
});
return { ...initObj };
}
}
複製代碼
將它和咱們的加強後的 useReducer 結合起來,咱們便擁有了一個幾乎能夠媲美 redux 的 reducer。
多是目前社區中得到 star 和關注最多的自定義 hooks 項目,提供了很是多的自定義 hooks(不少是香的) react-use
在 React hooks 雨後春筍般的請求庫中,最爲亮眼的當屬於 swr。詳情請見官方 github 倉庫 swr
一個很好用的 react-hook-form 表單庫。詳情請見官方 github 倉庫 react-hook-form