學習 React Hooks 可能會遇到的五個靈魂問題
- 2019 年 11 月 8 日
- 筆記
作者:橘子小睿 原文鏈接:https://zhuanlan.zhihu.com/p/85969406
之前寫新手學習 react 迷惑的點(完整版)反響還不錯,很多讀者要求寫一篇 React Hooks 相關的,最近正好在知乎上看到一篇關於可能在使用 hooks 的疑問,我覺得寫得很棒,所以找作者橘子小睿拿到授權,分享給大家,下面是正文:
正文
從 React Hooks 正式發布到現在,我一直在項目使用它。但是,在使用 Hooks 的過程中,我也進入了一些誤區,導致寫出來的程式碼隱藏 bug 並且難以維護。這篇文章中,我會具體分析這些問題,並總結一些好的實踐,以供大家參考。
問題一:我該使用單個 state 變數還是多個 state 變數?
useState
的出現,讓我們可以使用多個 state 變數來保存 state,比如:
const [width, setWidth] = useState(100); const [height, setHeight] = useState(100); const [left, setLeft] = useState(0); const [top, setTop] = useState(0);
但同時,我們也可以像 Class 組件的 this.state
一樣,將所有的 state 放到一個 object
中,這樣只需一個 state 變數即可:
const [state, setState] = useState({ width: 100, height: 100, left: 0, top: 0 });
那麼問題來了,到底該用單個 state 變數還是多個 state 變數呢?
如果使用單個 state 變數,每次更新 state 時需要合併之前的 state。因為 useState
返回的 setState
會替換原來的值。這一點和 Class 組件的 this.setState
不同。this.setState
會把更新的欄位自動合併到 this.state
對象中。
const handleMouseMove = (e) => { setState((prevState) => ({ ...prevState, left: e.pageX, top: e.pageY, })) };
使用多個 state 變數可以讓 state 的粒度更細,更易於邏輯的拆分和組合。比如,我們可以將關聯的邏輯提取到自定義 Hook 中:
function usePosition() { const [left, setLeft] = useState(0); const [top, setTop] = useState(0); useEffect(() => { // ... }, []); return [left, top, setLeft, setTop]; }
我們發現,每次更新 left
時 top
也會隨之更新。因此,把 top
和 left
拆分為兩個 state 變數顯得有點多餘。
在使用 state 之前,我們需要考慮狀態拆分的「粒度」問題。如果粒度過細,程式碼就會變得比較冗餘。如果粒度過粗,程式碼的可復用性就會降低。那麼,到底哪些 state 應該合併,哪些 state 應該拆分呢?我總結了下面兩點:
- 將完全不相關的 state 拆分為多組 state。比如
size
和position
。 - 如果某些 state 是相互關聯的,或者需要一起發生改變,就可以把它們合併為一組 state。比如
left
和top
。
function Box() { const [position, setPosition] = usePosition(); const [size, setSize] = useState({width: 100, height: 100}); // ... } function usePosition() { const [position, setPosition] = useState({left: 0, top: 0}); useEffect(() => { // ... }, []); return [position, setPosition]; }
問題二:deps 依賴過多,導致 Hooks 難以維護?
使用 useEffect
hook 時,為了避免每次 render 都去執行它的 callback,我們通常會傳入第二個參數「dependency array」(下面統稱為依賴數組)。這樣,只有當依賴數組發生變化時,才會執行 useEffect
的回調函數。
function Example({id, name}) { useEffect(() => { console.log(id, name); }, [id, name]); }
在上面的例子中,只有當 id
或 name
發生變化時,才會列印日誌。依賴數組中必須包含在 callback 內部用到的所有參與 React 數據流的值,比如 state
、props
以及它們的衍生物。如果有遺漏,可能會造成 bug。這其實就是 JS 閉包問題,對閉包不清楚的同學可以自行 google,這裡就不展開了。
function Example({id, name}) { useEffect(() => { // 由於依賴數組中不包含 name,所以當 name 發生變化時,無法列印日誌 console.log(id, name); }, [id]); }
在 React 中,除了 useEffect 外,接收依賴數組作為參數的 Hook 還有 useMemo
、useCallback
和 useImperativeHandle
。我們剛剛也提到了,依賴數組中千萬不要遺漏回調函數內部依賴的值。但是,如果依賴數組依賴了過多東西,可能導致程式碼難以維護。我在項目中就看到了這樣一段程式碼:
const refresh = useCallback(() => { // ... }, [name, searchState, address, status, personA, personB, progress, page, siz
不要說內部邏輯了,光是看到這一堆依賴就令人頭大!如果項目中到處都是這樣的程式碼,可想而知維護起來多麼痛苦。如何才能避免寫出這樣的程式碼呢?
首先,你需要重新思考一下,這些 deps 是否真的都需要?看下面這個例子:
function Example({id}) { const requestParams = useRef({}); requestParams.current = {page: 1, size: 20, id}; const refresh = useCallback(() => { doRefresh(requestParams.current); }, []); useEffect(() => { id && refresh(); }, [id, refresh]); // 思考這裡的 deps list 是否合理? }
雖然 useEffect
的回調函數依賴了 id
和 refresh
方法,但是觀察 refresh
方法可以發現,它在首次 render 被創建之後,永遠不會發生改變了。因此,把它作為 useEffect
的 deps 是多餘的。
其次,如果這些依賴真的都是需要的,那麼這些邏輯是否應該放到同一個 hook 中?
function Example({id, name, address, status, personA, personB, progress}) { const [page, setPage] = useState(); const [size, setSize] = useState(); const doSearch = useCallback(() => { // ... }, []); const doRefresh = useCallback(() => { // ... }, []); useEffect(() => { id && doSearch({name, address, status, personA, personB, progress}); page && doRefresh({name, page, size}); }, [id, name, address, status, personA, personB, progress, page, size]); }
可以看出,在 useEffect
中有兩段邏輯,這兩段邏輯是相互獨立的,因此我們可以將這兩段邏輯放到不同 useEffect
中:
useEffect(() => { id && doSearch({name, address, status, personA, personB, progress}); }, [id, name, address, status, personA, personB, progress]); useEffect(() => { page && doRefresh({name, page, size}); }, [name, page, size]);
如果邏輯無法繼續拆分,但是依賴數組還是依賴了過多東西,該怎麼辦呢?就比如我們上面的程式碼:
useEffect(() => { id && doSearch({name, address, status, personA, personB, progress}); }, [id, name, address, status, personA, personB, progress]);
這段程式碼中的 useEffect
依賴了七個值,還是偏多了。仔細觀察上面的程式碼,可以發現這些值都是「過濾條件」的一部分,通過這些條件可以過濾頁面上的數據。因此,我們可以將它們看做一個整體,也就是我們前面講過的合併 state:
const [filters, setFilters] = useState({ name: "", address: "", status: "", personA: "", personB: "", progress: "" }); useEffect(() => { id && doSearch(filters); }, [id, filters]);
如果 state 不能合併,在 callback 內部又使用了 setState
方法,那麼可以考慮使用 setState
callback 來減少一些依賴。比如:
const useValues = () => { const [values, setValues] = useState({ data: {}, count: 0 }); const [updateData] = useCallback( (nextData) => { setValues({ data: nextData, count: values.count + 1 // 因為 callback 內部依賴了外部的 values 變數,所以必須在依賴數組中指定它 }); }, [values], ); return [values, updateData]; };
上面的程式碼中,我們必須在 useCallback
的依賴數組中指定 values
,否則我們無法在 callback 中獲取到最新的 values
狀態。但是,通過 setState
回調函數,我們不用再依賴外部的 values
變數,因此也無需在依賴數組中指定它。就像下面這樣:
const useValues = () => { const [values, setValues] = useState({}); const [updateData] = useCallback((nextData) => { setValues((prevValues) => ({ data: nextData, count: prevValues.count + 1, // 通過 setState 回調函數獲取最新的 values 狀態,這時 callback 不再依賴於外部的 values 變數了,因此依賴數組中不需要指定任何值 })); }, []); // 這個 callback 永遠不會重新創建 return [values, updateData]; };
最後,還可以通過 ref
來保存可變變數。以前我們只把 ref
用作保持 DOM 節點引用的工具,可 useRef
Hook 能做的事情遠不止如此。我們可以用它來保存一些值的引用,並對它進行讀寫。舉個例子:
const useValues = () => { const [values, setValues] = useState({}); const latestValues = useRef(values); latestValues.current = values; const [updateData] = useCallback((nextData) => { setValues({ data: nextData, count: latestValues.current.count + 1, }); }, []); return [values, updateData]; };
在使用 ref
時要特別小心,因為它可以隨意賦值,所以一定要控制好修改它的方法。特別是一些底層模組,在封裝的時候千萬不要直接暴露 ref
,而是提供一些修改它的方法。
說了這麼多,歸根到底都是為了寫出更加清晰、易於維護的程式碼。如果發現依賴數組依賴過多,我們就需要重新審視自己的程式碼。
- 依賴數組依賴的值最好不要超過 3 個,否則會導致程式碼會難以維護。
- 如果發現依賴數組依賴的值過多,我們應該採取一些方法來減少它。
- 去掉不必要的依賴。
- 將 Hook 拆分為更小的單元,每個 Hook 依賴於各自的依賴數組。
- 通過合併相關的 state,將多個依賴值聚合為一個。
- 通過
setState
回調函數獲取最新的 state,以減少外部依賴。 - 通過
ref
來讀取可變變數的值,不過需要注意控制修改它的途徑。
問題三:該不該使用 useMemo
?
該不該使用 useMemo
?對於這個問題,有的人從來沒有思考過,有的人甚至不覺得這是個問題。不管什麼情況,只要用 useMemo
或者 useCallback
「包裹一下」,似乎就能使應用遠離性能的問題。但真的是這樣嗎?有的時候 useMemo
沒有任何作用,甚至還會影響應用的性能。
為什麼這麼說呢?首先,我們需要知道 useMemo
本身也有開銷。useMemo
會「記住」一些值,同時在後續 render 時,將依賴數組中的值取出來和上一次記錄的值進行比較,如果不相等才會重新執行回調函數,否則直接返回「記住」的值。這個過程本身就會消耗一定的記憶體和計算資源。因此,過度使用 useMemo
可能會影響程式的性能。
要想合理使用 useMemo
,我們需要搞清楚 useMemo
適用的場景:
- 有些計算開銷很大,我們就需要「記住」它的返回值,避免每次 render 都去重新計算。
- 由於值的引用發生變化,導致下游組件重新渲染,我們也需要「記住」這個值。
讓我們來看個例子:
interface IExampleProps { page: number; type: string; } const Example = ({page, type}: IExampleProps) => { const resolvedValue = useMemo(() => { return getResolvedValue(page, type); }, [page, type]); return <ExpensiveComponent resolvedValue={resolvedValue}/>; };
在上面的例子中,渲染 ExpensiveComponent
的開銷很大。所以,當 resolvedValue
的引用發生變化時,作者不想重新渲染這個組件。因此,作者使用了 useMemo
,避免每次 render 重新計算 resolvedValue
,導致它的引用發生改變,從而使下游組件 re-render。
這個擔憂是正確的,但是使用 useMemo
之前,我們應該先思考兩個問題:
- 傳遞給
useMemo
的函數開銷大不大?在上面的例子中,就是考慮getResolvedValue
函數的開銷大不大。JS 中大多數方法都是優化過的,比如Array.map
、Array.forEach
等。如果你執行的操作開銷不大,那麼就不需要記住返回值。否則,使用useMemo
本身的開銷就可能超過重新計算這個值的開銷。因此,對於一些簡單的 JS 運算來說,我們不需要使用useMemo
來「記住」它的返回值。 - 當輸入相同時,「記憶」值的引用是否會發生改變?在上面的例子中,就是當
page
和type
相同時,resolvedValue
的引用是否會發生改變?這裡我們就需要考慮resolvedValue
的類型了。如果resolvedValue
是一個對象,由於我們項目上使用「函數式編程」,每次函數調用都會產生一個新的引用。但是,如果resolvedValue
是一個原始值(string
,boolean
,null
,undefined
,number
,symbol
),也就不存在「引用」的概念了,每次計算出來的這個值一定是相等的。也就是說,ExpensiveComponent
組件不會被重新渲染。
因此,如果 getResolvedValue
的開銷不大,並且 resolvedValue
返回一個字元串之類的原始值,那我們完全可以去掉 useMemo
,就像下面這樣:
interface IExampleProps { page: number; type: string; } const Example = ({page, type}: IExampleProps) => { const resolvedValue = getResolvedValue(page, type); return <ExpensiveComponent resolvedValue={resolvedValue}/>; };
還有一個誤區就是對創建函數開銷的評估。有的人覺得在 render 中創建函數可能會開銷比較大,為了避免函數多次創建,使用了 useMemo
或者 useCallback
。但是對於現代瀏覽器來說,創建函數的成本微乎其微。因此,我們沒有必要使用 useMemo
或者 useCallback
去節省這部分性能開銷。當然,如果是為了保證每次 render 時回調的引用相等,你可以放心使用 useMemo
或者 useCallback
。
const Example = () => { const onSubmit = useCallback(() => { // 考慮這裡的 useCallback 是否必要? doSomething(); }, []); return <form onSubmit={onSubmit}></form>; };
我之前看過一篇文章(鏈接在文章的最後),這篇文章中提到,如果只是想在重新渲染時保持值的引用不變,更好的方法是使用 useRef
,而不是 useMemo
。我並不同意這個觀點。讓我們來看個例子:
// 使用 useMemo function Example() { const users = useMemo(() => [1, 2, 3], []); return <ExpensiveComponent users={users} /> } // 使用 useRef function Example() { const {current: users} = useRef([1, 2, 3]); return <ExpensiveComponent users={users} /> }
在上面的例子中,我們用 useMemo
來「記住」users
數組,不是因為數組本身的開銷大,而是因為 users
的引用在每次 render 時都會發生改變,從而導致子組件 ExpensiveComponent
重新渲染(可能會帶來較大開銷)。
作者認為從語義上不應該使用 useMemo
,而是應該使用 useRef
,否則會消耗更多的記憶體和計算資源。雖然在 React 中 useRef
和 useMemo
的實現有一點差別,但是當 useMemo
的依賴數組為空數組時,它和 useRef
的開銷可以說相差無幾。useRef
甚至可以直接用 useMemo
來實現,就像下面這樣:
const useRef = (v) => { return useMemo(() => ({current: v}), []); };
因此,我認為使用 useMemo
來保持值的引用一致沒有太大問題。
在編寫自定義 Hook 時,返回值一定要保持引用的一致性。因為你無法確定外部要如何使用它的返回值。如果返回值被用做其他 Hook 的依賴,並且每次 re-render 時引用不一致(當值相等的情況),就可能會產生 bug。比如:
function Example() { const data = useData(); const [dataChanged, setDataChanged] = useState(false); useEffect(() => { setDataChanged((prevDataChanged) => !prevDataChanged); // 當 data 發生變化時,調用 setState。如果 data 值相同而引用不同,就可能會產生非預期的結果。 }, [data]); console.log(dataChanged); return <ExpensiveComponent data={data} />; } const useData = () => { // 獲取非同步數據 const resp = getAsyncData([]); // 處理獲取到的非同步數據,這裡使用了 Array.map。因此,即使 data 相同,每次調用得到的引用也是不同的。 const mapper = (data) => data.map((item) => ({...item, selected: false})); return resp ? mapper(resp) : resp; };
在上面的例子中,我們通過 useData
Hook 獲取了 data
。每次 render 時 data
的值沒有發生變化,但是引用卻不一致。如果把 data
用到 useEffect
的依賴數組中,就可能產生非預期的結果。另外,由於引用的不同,也會導致 ExpensiveComponent
組件 re-render,產生性能問題。
如果因為 prop 的值相同而引用不同,從而導致子組件發生 re-render,不一定會造成性能問題。因為 Virtual DOM re-render ≠ DOM re-render。但是當子組件特別大時,Virtual DOM 的 Diff 開銷也很大。因此,還是應該盡量避免子組件 re-render。
因此,在使用 useMemo
之前,我們不妨先問自己幾個問題:
- 要記住的函數開銷很大嗎?
- 返回的值是原始值嗎?
- 記憶的值會被其他 Hook 或者子組件用到嗎?
回答出上面這幾個問題,判斷是否應該使用 useMemo
也就不再困難了。不過在實際項目中,還是最好定義出一套統一的規範,方便團隊中多人協作。比如第一個問題,開銷很大如何定義?如果沒有明確的標準,執行起來會非常困難。因此,我總結了下面幾條規則:
- 如果返回的值是原始值:
string
,boolean
,null
,undefined
,number
,symbol
(不包括動態聲明的 Symbol),則不需要使用useMemo
。 - 對於組件內部用到的 object、array、函數等,如果沒有用到其他 Hook 的依賴數組中,或者造成子組件 re-render,可以不使用
useMemo
。 - 自定義 Hook 中暴露出來的 object、array、函數等,都應該使用
useMemo
。以確保當值相同時,引用不發生變化。
問題四:Hooks 能替代高階組件和 Render Props 嗎?
在 Hooks 出現之前,我們有兩種方法可以復用組件邏輯:Render Props[1] 和高階組件[2]。但是這兩種方法都可能會造成 JSX「嵌套地域」的問題。Hooks 的出現,讓組件邏輯的復用變得更簡單,同時解決了「嵌套地域」的問題。Hooks 之於 React 就像 async / await 之於 Promise 一樣。
那 Hooks 能替代高階組件和 Render Props 嗎?官方給出的回答是,在高階組件或者 Render Props 只渲染一個子組件時,Hook 提供了一種更簡單的方式。不過在我看來,Hooks 並不能完全替代 Render Props 和高階組件。接下來,我們會詳細分析這個問題。
高階組件 HOC
高階組件是一個函數,它接受一個組件作為參數,返回一個新的組件。
function enhance(Comp) { // 增加一些其他的功能 return class extends Component { // ... render() { return <Comp />; } }; }
高階組件採用了裝飾器模式,讓我們可以增強原有組件的功能,並且不破壞它原有的特性。例如:
const RedButton = withStyles({ root: { background: "red", }, })(Button);
在上面的程式碼中,我們希望保留 Button
組件的邏輯,但同時我們又想使用它原有的樣式。因此,我們通過 withStyles
這個高階組件注入了自定義的樣式,並且生成了一個新的組件 RedButton
。
Render Props
Render Props 通過父組件將可復用邏輯封裝起來,並把數據提供給子組件。至於子組件拿到數據之後要怎麼渲染,完全由子組件自己決定,靈活性非常高。而高階組件中,渲染結果是由父組件決定的。Render Props 不會產生新的組件,而且更加直觀的體現了「父子關係」。
<Parent> {(data) => { // 你父親已經把江山給你打好了,並給你留下了一堆金幣,至於怎麼花就看你自己了 return <Child data={data} />; }} </Parent>
Render Props 作為 JSX 的一部分,可以很方便地利用 React 生命周期和 Props、State 來進行渲染,在渲染上有著非常高的自由度。同時,它不像 Hooks 需要遵守一些規則,你可以放心大膽的在它裡面使用 if / else、map 等各類操作。
在大部分情況下,高階組件和 Render Props 是可以相互轉換的,也就是說用高階組件能實現的,用 Render Props 也能實現。只不過在不同的場景下,哪種方式使用起來簡單一點罷了。
將上面 HOC 的例子改成 Render Props,使用起來確實要「麻煩」一點:
<RedButton> {(styles)=>( <Button styles={styles}/> )} </RedButton>
小結
沒有 Hooks 之前,高階組件和 Render Props 本質上都是將復用邏輯提升到父組件中。而 Hooks 出現之後,我們將復用邏輯提取到組件頂層,而不是強行提升到父組件中。這樣就能夠避免 HOC 和 Render Props 帶來的「嵌套地域」。但是,像 Context 的 <Provider/>
和 <Consumer/>
這樣有父子層級關係(樹狀結構關係)的,還是只能使用 Render Props 或者 HOC。
對於 Hooks、Render Props 和高階組件來說,它們都有各自的使用場景:
- Hooks:
- 替代 Class 的大部分用例,除了
getSnapshotBeforeUpdate
和componentDidCatch
還不支援。 - 提取復用邏輯。除了有明確父子關係的,其他場景都可以使用 Hooks。
- Render Props:在組件渲染上擁有更高的自由度,可以根據父組件提供的數據進行動態渲染。適合有明確父子關係的場景。
- 高階組件:適合用來做注入,並且生成一個新的可復用組件。適合用來寫插件。
不過,能使用 Hooks 的場景還是應該優先使用 Hooks,其次才是 Render Props 和 HOC。當然,Hooks、Render Props 和 HOC 不是對立的關係。我們既可以用 Hook 來寫 Render Props 和 HOC,也可以在 HOC 中使用 Render Props 和 Hooks。
問題五:使用 Hooks 時還有哪些好的實踐?
- 若 Hook 類型相同,且依賴數組一致時,應該合併成一個 Hook。否則會產生更多開銷。
const dataA = useMemo(() => { return getDataA(); }, [A, B]); const dataB = useMemo(() => { return getDataB(); }, [A, B]); // 應該合併為 const [dataA, dataB] = useMemo(() => { return [getDataA(), getDataB()] }, [A, B]);
- 參考原生 Hooks 的設計,自定義 Hooks 的返回值可以使用 Tuple 類型,更易於在外部重命名。但如果返回值的數量超過三個,還是建議返回一個對象。
export const useToggle = (defaultVisible: boolean = false) => { const [visible, setVisible] = useState(defaultVisible); const show = () => setVisible(true); const hide = () => setVisible(false); return [visible, show, hide] as [typeof visible, typeof show, typeof hide]; }; const [isOpen, open, close] = useToggle(); // 在外部可以更方便地修改名字 const [visible, show, hide] = useToggle();
ref
不要直接暴露給外部使用,而是提供一個修改值的方法。- 在使用
useMemo
或者useCallback
時,確保返回的函數只創建一次。也就是說,函數不會根據依賴數組的變化而二次創建。舉個例子:
export const useCount = () => { const [count, setCount] = useState(0); const [increase, decrease] = useMemo(() => { const increase = () => { setCount(count + 1); }; const decrease = () => { setCount(count - 1); }; return [increase, decrease]; }, [count]); return [count, increase, decrease]; };
在 useCount
Hook 中, count
狀態的改變會讓 useMemo
中的 increase
和 decrease
函數被重新創建。由於閉包特性,如果這兩個函數被其他 Hook 用到了,我們應該將這兩個函數也添加到相應 Hook 的依賴數組中,否則就會產生 bug。比如:
function Counter() { const [count, increase] = useCount(); useEffect(() => { const handleClick = () => { increase(); // 執行後 count 的值永遠都是 1 }; document.body.addEventListener("click", handleClick); return () => { document.body.removeEventListener("click", handleClick); }; }, []); return <h1>{count}</h1>; }
在 useCount
中,increase
會隨著 count
的變化而被重新創建。但是 increase
被重新創建之後, useEffect
並不會再次執行,所以 useEffect
中取到的 increase
永遠都是首次創建時的 increase
。而首次創建時 count
的值為 0,因此無論點擊多少次, count
的值永遠都是 1。
那把 increase
函數放到 useEffect
的依賴數組中不就好了嗎?事實上,這會帶來更多問題:
increase
的變化會導致頻繁地綁定事件監聽,以及解除事件監聽。- 需求是只在組件 mount 時執行一次
useEffect
,但是increase
的變化會導致useEffect
多次執行,不能滿足需求。
如何解決這些問題呢?
一、通過 setState
回調,讓函數不依賴外部變數。例如:
export const useCount = () => { const [count, setCount] = useState(0); const [increase, decrease] = useMemo(() => { const increase = () => { setCount((latestCount) => latestCount + 1); }; const decrease = () => { setCount((latestCount) => latestCount - 1); }; return [increase, decrease]; }, []); // 保持依賴數組為空,這樣 increase 和 decrease 方法都只會被創建一次 return [count, increase, decrease]; };
二、通過 ref
來保存可變變數。例如:
export const useCount = () => { const [count, setCount] = useState(0); const countRef = useRef(count); countRef.current = count; const [increase, decrease] = useMemo(() => { const increase = () => { setCount(countRef.current + 1); }; const decrease = () => { setCount(countRef.current - 1); }; return [increase, decrease]; }, []); // 保持依賴數組為空,這樣 increase 和 decrease 方法都只會被創建一次 return [count, increase, decrease]; };
最後
我們總結了在實踐中一些常見的問題,並提出了一些解決方案。最後讓我們再來回顧一下:
- 將完全不相關的 state 拆分為多組 state。
- 如果某些 state 是相互關聯的,或者需要一起發生改變,就可以把它們合併為一組 state。
- 依賴數組依賴的值最好不要超過 3 個,否則會導致程式碼會難以維護。
- 如果發現依賴數組依賴的值過多,我們應該採取一些方法來減少它。
- 去掉不必要的依賴。
- 將 Hook 拆分為更小的單元,每個 Hook 依賴於各自的依賴數組。
- 通過合併相關的 state,將多個依賴值聚合為一個。
- 通過
setState
回調函數獲取最新的 state,以減少外部依賴。 - 通過
ref
來讀取可變變數的值,不過需要注意控制修改它的途徑。 - 為了確保不濫用
useMemo
,我們定義了下面幾條規則: - 如果返回的值是原始值:
string
,boolean
,null
,undefined
,number
,symbol
(不包括動態聲明的 Symbol),則不需要使用useMemo
。 - 對於組件內部用到的 object、array、函數等,如果沒有用到其他 Hook 的依賴數組中,或者造成子組件 re-render,可以不使用
useMemo
。 - 自定義 Hook 中暴露出來的 object、array、函數等,都應該使用
useMemo
。以確保當值相同時,引用不發生變化。 - Hooks、Render Props 和高階組件都有各自的使用場景,具體使用哪一種要看實際情況。
- 若 Hook 類型相同,且依賴數組一致時,應該合併成一個 Hook。
- 自定義 Hooks 的返回值可以使用 Tuple 類型,更易於在外部重命名。如果返回的值過多,則不建議使用。
ref
不要直接暴露給外部使用,而是提供一個修改值的方法。- 在使用
useMemo
或者useCallback
時,可以藉助ref
或者setState
callback,確保返回的函數只創建一次。也就是說,函數不會根據依賴數組的變化而二次創建。
參考文章:
You』re overusing useMemo: Rethinking Hooks memoization[3]
參考資料
[1]
Render Props: https://link.zhihu.com/?target=https%3A//reactjs.org/docs/render-props.html
[2]
高階組件: https://link.zhihu.com/?target=https%3A//reactjs.org/docs/higher-order-components.html
[3]
You』re overusing useMemo: Rethinking Hooks memoization: https://link.zhihu.com/?target=https%3A//blog.logrocket.com/rethinking-hooks-memoization/%3Ffrom%3Dsinglemessage%26isappinstalled%3D0