使用 React Hooks 時要避免的6個錯誤

語言: CN / TW / HK

theme: cyanosis highlight: agate


這是我參與11月更文挑戰的第4天,活動詳情檢視:2021最後一次更文挑戰


今天來看看在使用React hooks時的一些坑,以及如何正確的使用避免這些坑。

問題概覽:

  1. 不要改變 hooks 的呼叫順序;
  2. 不要使用舊的狀態;
  3. 不要建立舊的閉包;
  4. 不要忘記清理副作用;
  5. 不要在不需要重新渲染時使用useState;
  6. 不要缺少useEffect依賴。

1. 不要改變 hooks 的呼叫順序

下面先來看一個例子: ```javascript const FetchGame = ({ id }) => { if (!id) { return '請選擇一個遊戲'; }

const [game, setGame] = useState({ name: '', description: '' });

useEffect(() => { const fetchGame = async () => { const response = await fetch(/api/game/${id}); const fetchedGame = await response.json(); setGame(fetchedGame); }; fetchGame(); }, [id]);

return (

Name: {game.name}
Description: {game.description}
); } ``` 這個元件接收一個引數id,在useEffect中會使用這個id作為引數去請求遊戲的資訊。並將獲取的資料儲存在狀態變數game中。 ​

當元件執行時,會獲取導資料並更新狀態。但是這個元件有一個警告: image.png 這裡是告訴我們,鉤子的執行是不正確的。因為當id為空時,元件會提示,並直接退出。如果id存在,就會呼叫useState和useEffect這兩個hook。這樣有條件的執行鉤子時就可能會導致意外並且難以除錯的錯誤。實際上,React hooks內部的工作方式要求元件在渲染時,總是以相同的順序來呼叫hook。 ​

這也就是React官方文件中所說的:不要在迴圈,條件或巢狀函式中呼叫 Hook, 確保總是在你的 React 函式的最頂層以及任何 return 之前呼叫他們。

解決這個問題最直接的辦法就是按照官方文件所說的,確保總是在你的 React 函式的最頂層以及任何 return 之前呼叫他們: ```javascript const FetchGame = ({ id }) => { const [game, setGame] = useState({ name: '', description: '' });

useEffect(() => { const fetchGame = async () => { const response = await fetch(/api/game/${id}); const fetchedGame = await response.json(); setGame(fetchedGame); }; id && fetchGame(); }, [id]);

if (!id) { return '請選擇一個遊戲'; }

return (

Name: {game.name}
Description: {game.description}
); } ``` 這樣,無論傳入的id是否為空,useState和useEffect總會以相同的順序來低啊用,這樣就不會出錯啦~ ​

React官方文件中的Hook規則:《Hook 規則》,可以使用外掛eslint-plugin-react-hooks來幫助我們檢查這些規則。

2. 不要使用舊的狀態

先來看一個計數器的例子: ```javascript const Increaser = () => { const [count, setCount] = useState(0);

const increase = useCallback(() => { setCount(count + 1); }, [count]);

const handleClick = () => { increase(); increase(); increase(); };

return ( <>

Counter: {count}

); } 這裡的handleClick方法會在點選按鈕後執行三次增加狀態變數count的操作。那麼點選一次是否會增加3呢?事實並非如此。點選按鈕之後,count只會增加1。問題就在於,當我們點選按鈕時,相當於下面的操作:javascript const handleClick = () => { setCount(count + 1); setCount(count + 1); setCount(count + 1); }; ``` 當第一次呼叫setCount(count + 1)時是沒有問題的,它會將count更新為1。接下來第2、3次呼叫setCount時,count還是使用了舊的狀態(count為0),所以也會計算出count為1。發生這種情況的原因就是狀態變數會在下一次渲染才更新。 ​

解決這個問題的辦法就是,使用函式的方式來更新狀態: ```javascript const Increaser = () => { const [count, setCount] = useState(0);

const increase = useCallback(() => { setCount(count => count + 1); }, [count]);

const handleClick = () => { increase(); increase(); increase(); };

return ( <>

Counter: {count}

); } 這樣改完之後,React就能拿到最新的值,當點選按鈕時,就會每次增加3。所以需要記住:**如果要使用當前狀態來計算下一個狀態,就要使用函式的式方式來更新狀態:**javascript setValue(prevValue => prevValue + someResult) ```

2. 不要建立舊的閉包

眾所周知,React Hooks是依賴閉包實現的。當使用接收一個回撥作為引數的鉤子時,比如: css useEffect(callback, deps) useCallback(callback, deps) 此時,我們就可能會建立一箇舊的閉包,該閉包會捕獲過時的狀態或者prop變數。這麼說可能有些抽象,下面來看一個例子,這個例子中,useEffect每2秒會列印一次count的值: ```javascript const WatchCount = () => { const [count, setCount] = useState(0);

useEffect(() => { setInterval(function log() { console.log(Count: ${count}); }, 2000); }, []);

const handleClick = () => setCount(count => count + 1);

return ( <>

Count: {count}

); }

``` 最終的輸出的結果如下:

image.png

可以看到,每次列印的count值都是0,和實際的count值並不一樣。為什麼會這樣呢?

在第一次渲染時應該沒啥問題,閉包log會將count打印出0。從第二次開始,每次當點選按鈕時,count會增加1,但是setInterval仍然呼叫的是從初次渲染中捕獲的count為0的舊的log閉包。log方法就是一箇舊的閉包,因為它捕獲的是一個過時的狀態變數count。 ​

這裡的解決方案就是,當count發生變化時,就重置定時器: ```javascript const WatchCount = () => { const [count, setCount] = useState(0);

useEffect(function() { const id = setInterval(function log() { console.log(Count: ${count}); }, 2000); return () => clearInterval(id); }, [count]);

const handleClick = () => setCount(count => count + 1);

return ( <>

Count: {count}

); } ``` 這樣,當狀態變數count發生變化時,就會更新閉包。為了防止閉包捕獲到舊值,就要確保在提供給hook的回撥中使用的prop或者state都被指定為依賴性。

4. 不要忘記清理副作用

有很多副作用,比如fetch請求、setTimeout等都是非同步的,如果不需要這些副作用或者元件在解除安裝時,不要忘記清理這些副作用。下面來看一個計數器的例子: ```javascript const DelayedIncreaser = () => { const [count, setCount] = useState(0); const [increase, setShouldIncrease] = useState(false);

useEffect(() => { if (increase) { setInterval(() => { setCount(count => count + 1) }, 1000); } }, [increase]);

return ( <>

Count: {count}

); }

const MyApp = () => { const [show, setShow] = useState(true);

return ( <> {show ? : null}
); } ``` 這個元件很簡單,就是在點選按鈕時,狀態變數count每秒會增加1。當我們點選+按鈕時,它會和我們預期的一樣。但是當我們點選“解除安裝”按鈕時,控制檯就會出現警告:

image.png

修復這個問題只需要使用useEffect來清理定時器即可: javascript useEffect(() => { if (increase) { const id = setInterval(() => { setCount(count => count + 1) }, 1000); return () => clearInterval(id); } }, [increase]); 當我們編寫一些副作用時,我們需要知道這個副作用是否需要清除。

5. 不要在不需要重新渲染時使用useState

在React hooks 中,我們可以使用useState hook來進行狀態的管理。雖然使用起來比較簡單,但是如果使用不恰當,就可能會出現意想不到的問題。來看下面的例子: ```javascript const Counter = () => { const [counter, setCounter] = useState(0);

const onClickCounter = () => { setCounter(counter => counter + 1); };

const onClickCounterRequest = () => { apiCall(counter); };

return (

); } ``` 在上面的元件中,有兩個按鈕,第一個按鈕會觸發計數器加一,第二個按鈕會根據當前的計數器狀態傳送一個請求。可以看到,狀態變數counter並沒有在渲染階段使用。所以,每次點選第一個按鈕時,都會有不需要的重新渲染。 ​

因此,當遇到這種需要在元件中使用一個變數在渲染中保持其狀態,並且不會觸發重新渲染時,那麼useRef會是一個更好的選擇,下面來對上面的例子使用useRef進行改編: ```javascript const Counter = () => { const counter = useRef(0);

const onClickCounter = () => { counter.current++; };

const onClickCounterRequest = () => { apiCall(counter.current); };

return (

); } ```

6. 不要缺少useEffect依賴

useEffect是React Hooks中最常用的Hook之一。預設情況下,它總是在每次重新渲染時執行。但這樣就可能會導致不必要的渲染。我們可以通過給useEffect設定依賴陣列來避免這些不必要的渲染。 ​

來看下面的例子: ```javascript const Counter = () => { const [count, setCount] = useState(0);

const showCount = (count) => { console.log("Count", count); };

useEffect(() => { showCount(count); }, []);

return (

Counter: {count}
); } ``` 這個元件可能沒有什麼實際的意義,只是列印了count的值。這時就會有一個警告:

image.png

這裡是說,useEffect缺少一個count依賴,這樣是不安全的。我們需要包含一個依賴項或者移除依賴陣列。否則useEffect中的程式碼可能會使用舊的值。 ```javascript const Counter = () => { const [count, setCount] = useState(0);

const showCount = (count) => { console.log("Count", count); };

useEffect(() => { showCount(count); }, [count]);

return (

Counter: {count}
); } 如果useEffect中沒有用到狀態變數count,那麼依賴項為空也會是安全的:javascript useEffect(() => { showCount(996); }, []); ``` 今天的分享就到這裡,如果覺得有用就來個三連吧~