前言
優點一、更容易複用程式碼下面我談一下我認為的react hooks的優缺點,優缺點通過和傳統的React.Component進行對比得出。
這點應該是react hooks最大的優點,它通過自定義hooks來複用狀態,從而解決了類元件有些時候難以複用邏輯的問題。hooks是怎麼解決這個複用的問題呢,具體如下:
每呼叫useHook一次都會生成一份獨立的狀態,這個沒有什麼黑魔法,函式每次呼叫都會開闢一份獨立的記憶體空間。雖然狀態(from useState)和副作用(useEffect)的存在依賴於元件,但它們可以在元件外部進行定義。這點是class component做不到的,你無法在外部宣告state和副作用(如componentDidMount)。上面這兩點,高階元件和renderProps也同樣能做到。但hooks實現起來的程式碼量更少,以及更直觀(程式碼可讀性)。
舉個例子,我們經常使用的antd-table,我們經常需要寫一些狀態:pagination={current:xxx,pageSize:xxx,total:xxx}。但許多場景只是簡單的表格,我們希望封裝一個高階元件,自帶這些狀態,並可以自動呼叫server去獲取remote data。
用高階元件來實現這樣它:
import { Table } from 'antd'import server from './api'@useTable(server)class App extends Component{ render(){ // useTable和tableProps的程式碼是分離的,但高階元件一多,程式碼會變得較難閱讀, // 你難以區分這個props是來自哪個高階元件,或者還是來自業務的父元件。 const { tableProps } = this.props; return ( <Table columns={[...]} // tableProps包含pagination, onChange, dataSource等屬性 {...tableProps} /> ) }}
用hooks來實現的話,會是:import { Table } from 'antd'import server from './api'function App { const { tableProps } = useTable(); return ( <Table columns={[...]} // tableProps包含pagination, onChange, dataSource等屬性 {...tableProps} /> )}/*相對比高階元件“祖父=>父=>子”的層層巢狀,hooks是這樣的: const { brother1 } = usehook1; const { brother2} = usehook2;*/
實現有以下幾點優勢:二、清爽的程式碼風格把"useTable"和輸出的值寫到一起,結構更清晰,更容易閱讀和維護。對比高階元件,程式碼量更少。
函數語言程式設計風格,函式式元件、狀態儲存在執行環境、每個功能都包裹在函式中,整體風格更清爽,更優雅。另外,對比類元件,函式元件裡面的unused狀態和unused-method更容易被發現。
三、程式碼量更少向props或狀態取值更加方便,函式元件的取值都從當前作用域直接獲取變數,而類元件需要先訪問例項引用this,再訪問其屬性或者方法,多了一步。更改狀態也變得更加簡單, this.setState({ count:xxx })變成 setCount(xxx)。因為減少了很多模板程式碼,特別是小元件寫起來更加省事,人們更願意去拆分元件。而元件粒度越細,則被複用的可能性越大。所以,hooks也在不知不覺中改變人們的開發習慣,提高專案的元件複用率。
缺點一、響應式的useEffect寫函式元件時,你不得不改變一些寫法習慣。你必須清楚程式碼中useEffect和useCallback等api的第二個引數“依賴項陣列”的改變時機,並且掌握上下文的useEffect的觸發時機。當邏輯較複雜的時候,useEffect觸發的次數,可能會被你預想的多。對比componentDidmount和componentDidUpdate,useEffect帶來的心智負擔更大。
二、狀態不同步這絕對是最大的缺點。函式的執行是獨立的,每個函式都有一份獨立的作用域。函式的變數是儲存在執行時的作用域裡面,當我們有非同步操作的時候,經常會碰到非同步回撥的變數引用是之前的,也就是舊的(這裡也可以理解成閉包)。
import React, { useState } from "react";const Counter = () => { const [counter, setCounter] = useState(0); const onAlertButtonClick = () => { setTimeout(() => { alert("Value: " + counter); }, 3000); }; return ( <div> <p>You clicked {counter} times.</p> <button onClick={() => setCounter(counter + 1)}>Click me</button> <button onClick={onAlertButtonClick}> Show me the value in 3 seconds </button> </div> );};export default Counter;
當你點選Show me the value in 3 seconds的後,緊接著點選Click me使得counter的值從0變成1。三秒後,定時器觸發,但alert出來的是0(舊值),但我們希望的結果是當前的狀態1。
這個問題在class component不會出現,因為class component的屬性和方法都存放在一個instance上,呼叫方式是:this.state.xxx和this.method()。因為每次都是從一個不變的instance上進行取值,所以不存在引用是舊的問題。
其實解決這個hooks的問題也可以參照類的instance。用useRef返回的immutable RefObject(current屬性是可變的)來儲存state,然後取值方式從counter變成了:counterRef.current。如下:
import React, { useState, useRef, useEffect } from "react";const Counter = () => { const [counter, setCounter] = useState(0); const counterRef = useRef(counter); const onAlertButtonClick = () => { setTimeout(() => { alert("Value: " + counterRef.current); }, 3000); }; useEffect(() => { counterRef.current = counter; }); return ( <div> <p>You clicked {counter} times.</p> <button onClick={() => setCounter(counter + 1)}>Click me</button> <button onClick={onAlertButtonClick}> Show me the value in 3 seconds </button> </div> );};export default Counter;
結果如我們所期待,alert的是當前的值1。
我們可以把這個過程封裝成一個custom hook,如下:
import { useEffect, useRef, useState } from "react";const useRefState = <T>(initialValue: T): [T, React.MutableRefObject<T>, React.Dispatch<React.SetStateAction<T>>] => { const [state, setState] = useState<T>(initialValue); const stateRef = useRef(state); useEffect(() => { stateRef.current = state; }, [state]); return [state, stateRef, setState];};export default useRefState;
儘管這個問題被巧妙地解決了,但它不優雅、hack味道濃,且丟失了函式程式設計風格。
怎麼避免react hooks的常見問題不要在useEffect裡面寫太多的依賴項,劃分這些依賴項成多個單一功能的useEffect。其實這點是遵循了軟體設計的“單一職責模式”。如果你碰到狀態不同步的問題,可以考慮下手動傳遞引數到函式。如:// showCount的count來自父級作用域 const [count,setCount] = useState(xxx); function showCount(){ console.log(count) } // showCount的count來自引數 const [count,setCount] = useState(xxx); function showCount(c){ console.log(c) }
但這個也只能解決一部分問題,很多時候你不得不使用上述的useRef方案。
重視eslint-plugin-react-hooks外掛的警告。複雜業務的時候,使用Component代替hooks。感想目前,我通常更偏向於用hooks來寫元件,但在複雜業務中,我會更傾向於用class Component或者兩者結合的方式。hooks會是未來的主流元件編寫方式,但目前來說它還不成熟。