React Hooks 你真的用對了嗎?

從 React Hooks 正式發佈到如今,我一直在項目使用它。可是,在使用 Hooks 的過程當中,我也進入了一些誤區,致使寫出來的代碼隱藏 bug 而且難以維護。這篇文章中,我會具體分析這些問題,並總結一些好的實踐,以供你們參考。javascript

問題一:我該使用單個 state 變量仍是多個 state 變量?

useState 的出現,讓咱們可使用多個 state 變量來保存 state,好比:html

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 變量便可:java

const [state, setState] = useState({
  width: 100,
  height: 100,
  left: 0,
  top: 0
});

那麼問題來了,到底該用單個 state 變量仍是多個 state 變量呢?react

若是使用單個 state 變量,每次更新 state 時須要合併以前的 state。由於 useState 返回的 setState 會替換原來的值。這一點和 Class 組件的 this.setState 不一樣。this.setState 會把更新的字段自動合併到 this.state 對象中。typescript

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];
}

咱們發現,每次更新 lefttop 也會隨之更新。所以,把 topleft 拆分爲兩個 state 變量顯得有點多餘。數組

在使用 state 以前,咱們須要考慮狀態拆分的「粒度」問題。若是粒度過細,代碼就會變得比較冗餘。若是粒度過粗,代碼的可複用性就會下降。那麼,到底哪些 state 應該合併,哪些 state 應該拆分呢?我總結了下面兩點:瀏覽器

  1. 將徹底不相關的 state 拆分爲多組 state。好比 sizeposition
  2. 若是某些 state 是相互關聯的,或者須要一塊兒發生改變,就能夠把它們合併爲一組 state。好比 lefttop
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]); 
}

在上面的例子中,只有當 idname 發生變化時,纔會打印日誌。依賴數組中必須包含在 callback 內部用到的全部參與 React 數據流的值,好比 stateprops 以及它們的衍生物。若是有遺漏,可能會形成 bug。這其實就是 JS 閉包問題,對閉包不清楚的同窗能夠自行 google,這裏就不展開了。app

function Example({id, name}) {
  useEffect(() => {
    // 因爲依賴數組中不包含 name,因此當 name 發生變化時,沒法打印日誌
    console.log(id, name); 
  }, [id]);
}

在 React 中,除了 useEffect 外,接收依賴數組做爲參數的 Hook 還有 useMemouseCallbackuseImperativeHandle。咱們剛剛也提到了,依賴數組中千萬不要遺漏回調函數內部依賴的值。可是,若是依賴數組依賴了過多東西,可能致使代碼難以維護。我在項目中就看到了這樣一段代碼:

const refresh = useCallback(() => {
  // ...
}, [name, searchState, address, status, personA, personB, progress, page, size]);

不要說內部邏輯了,光是看到這一堆依賴就使人頭大!若是項目中處處都是這樣的代碼,可想而知維護起來多麼痛苦。如何才能避免寫出這樣的代碼呢?

首先,你須要從新思考一下,這些 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 的回調函數依賴了 idrefresh 方法,可是觀察 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 組件包裹了 React.memo

在上面的例子中,渲染 ExpensiveComponent 的開銷很大。因此,當 resolvedValue 的引用發生變化時,做者不想從新渲染這個組件。所以,做者使用了 useMemo,避免每次 render 從新計算 resolvedValue,致使它的引用發生改變,從而使下游組件 re-render。

這個擔心是正確的,可是使用 useMemo 以前,咱們應該先思考兩個問題:

  1. 傳遞給 useMemo 的函數開銷大不大?在上面的例子中,就是考慮 getResolvedValue 函數的開銷大不大。JS 中大多數方法都是優化過的,好比 Array.mapArray.forEach 等。若是你執行的操做開銷不大,那麼就不須要記住返回值。不然,使用 useMemo 自己的開銷就可能超太重新計算這個值的開銷。所以,對於一些簡單的 JS 運算來講,咱們不須要使用 useMemo 來「記住」它的返回值。
  2. 當輸入相同時,「記憶」值的引用是否會發生改變?在上面的例子中,就是當 pagetype 相同時,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 中 useRefuseMemo 的實現有一點差異,可是當 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 以前,咱們不妨先問本身幾個問題:

  1. 要記住的函數開銷很大嗎?
  2. 返回的值是原始值嗎?
  3. 記憶的值會被其餘 Hook 或者子組件用到嗎?

回答出上面這幾個問題,判斷是否應該使用 useMemo 也就再也不困難了。不過在實際項目中,仍是最好定義出一套統一的規範,方便團隊中多人協做。好比第一個問題,開銷很大如何定義?若是沒有明確的標準,執行起來會很是困難。所以,我總結了下面一些規則:

1、應該使用 useMemo 的場景

  1. 保持引用相等:

    • 對於組件內部用到的 object、array、函數等,若是用在了其餘 Hook 的依賴數組中,或者做爲 props 傳遞給了下游組件,應該使用 useMemo
    • 自定義 Hook 中暴露出來的 object、array、函數等,都應該使用 useMemo 。以確保當值相同時,引用不發生變化。
    • 使用 Context 時,若是 Provider 的 value 中定義的值(第一層)發生了變化,即使用了 Pure Component 或者 React.memo,仍然會致使子組件 re-render。這種狀況下,仍然建議使用 useMemo 保持引用的一致性。
  2. 計算成本很高

    • 好比 cloneDeep 一個很大而且層級很深的數據

2、無需使用 useMemo 的場景

  1. 若是返回的值是原始值: string, boolean, null, undefined, number, symbol(不包括動態聲明的 Symbol),通常不須要使用 useMemo
  2. 僅在組件內部用到的 object、array、函數等(沒有做爲 props 傳遞給子組件),且沒有用到其餘 Hook 的依賴數組中,通常不須要使用 useMemo

問題四:Hooks 能替代高階組件和 Render Props 嗎?

在 Hooks 出現以前,咱們有兩種方法能夠複用組件邏輯:Render Props高階組件。可是這兩種方法均可能會形成 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 的大部分用例,除了 getSnapshotBeforeUpdatecomponentDidCatch 還不支持。
    • 提取複用邏輯。除了有明確父子關係的,其餘場景均可以使用 Hooks。
  • Render Props:在組件渲染上擁有更高的自由度,能夠根據父組件提供的數據進行動態渲染。適合有明確父子關係的場景。
  • 高階組件:適合用來作注入,而且生成一個新的可複用組件。適合用來寫插件。

不過,能使用 Hooks 的場景仍是應該優先使用 Hooks,其次纔是 Render Props 和 HOC。固然,Hooks、Render Props 和 HOC 不是對立的關係。咱們既能夠用 Hook 來寫 Render Props 和 HOC,也能夠在 HOC 中使用 Render Props 和 Hooks。

問題五: 使用 Hooks 時還有哪些好的實踐?

1.若 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]);

2.參考原生 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();

3.ref 不要直接暴露給外部使用,而是提供一個修改值的方法。

4.在使用 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 中的 increasedecrease 函數被從新建立。因爲閉包特性,若是這兩個函數被其餘 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 屢次執行,不能知足需求。

如何解決這些問題呢?

1、經過 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];
};

2、經過 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];
};

最後

咱們總結了在實踐中一些常見的問題,並提出了一些解決方案。最後讓咱們再來回顧一下:

  1. 將徹底不相關的 state 拆分爲多組 state。
  2. 若是某些 state 是相互關聯的,或者須要一塊兒發生改變,就能夠把它們合併爲一組 state。
  3. 依賴數組依賴的值最好不要超過 3 個,不然會致使代碼會難以維護。
  4. 若是發現依賴數組依賴的值過多,咱們應該採起一些方法來減小它。

    • 去掉沒必要要的依賴。
    • 將 Hook 拆分爲更小的單元,每一個 Hook 依賴於各自的依賴數組。
    • 經過合併相關的 state,將多個依賴值聚合爲一個。
    • 經過 setState 回調函數獲取最新的 state,以減小外部依賴。
    • 經過 ref 來讀取可變變量的值,不過須要注意控制修改它的途徑。
  5. 爲了確保不濫用 useMemo,咱們定義了下面幾條規則:

    • 若是返回的值是原始值: string, boolean, null, undefined, number, symbol(不包括動態聲明的 Symbol),則不須要使用 useMemo
    • 對於組件內部用到的 object、array、函數等,若是沒有用到其餘 Hook 的依賴數組中,或者形成子組件 re-render,能夠不使用 useMemo
    • 自定義 Hook 中暴露出來的 object、array、函數等,都應該使用 useMemo 。以確保當值相同時,引用不發生變化。
  6. Hooks、Render Props 和高階組件都有各自的使用場景,具體使用哪種要看實際狀況。
  7. 若 Hook 類型相同,且依賴數組一致時,應該合併成一個 Hook。
  8. 自定義 Hooks 的返回值可使用 Tuple 類型,更易於在外部重命名。若是返回的值過多,則不建議使用。
  9. ref 不要直接暴露給外部使用,而是提供一個修改值的方法。
  10. 在使用 useMemo 或者 useCallback 時,能夠藉助 ref 或者 setState callback,確保返回的函數只建立一次。也就是說,函數不會根據依賴數組的變化而二次建立。

參考文章:

You’re overusing useMemo: Rethinking Hooks memoization

相關文章
相關標籤/搜索