今天來看看在使用React hooks時的一些坑,以及如何正确的使用避免這些坑。
問題概覽:
-
- 不要改變 hooks 的調用順序;
- 不要使用舊的狀态;
- 不要建立舊的閉包;
- 不要忘記清理副作用;
- 不要在不需要重新渲染時使用useState;
- 不要缺少useEffect依賴。
1. 不要改變 hooks 的調用順序
下面先來看一個例子:
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 (
<div>
<div>Name: {game.name}</div>
<div>Description: {game.description}</div>
</div>
);
}
這個元件接收一個參數id,在useEffect中會使用這個id作為參數去請求遊戲的資訊。并将擷取的資料儲存在狀态變量game中。
當元件執行時,會擷取到資料并更新狀态。但是這個元件有一個警告:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SO2MDM1UDZjNjN2gDNkNGMyYzX2ADM0gTM2EzLcJDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
這裡是告訴我們,鈎子的執行是不正确的。因為當id為空時,元件會提示,并直接退出。如果id存在,就會調用useState和useEffect這兩個hook。這樣有條件的執行鈎子時就可能會導緻意外并且難以調試的錯誤。實際上,React hooks内部的工作方式要求元件在渲染時,總是以相同的順序來調用hook。
這也就是React官方文檔中所說的:不要在循環,條件或嵌套函數中調用 Hook, 確定總是在你的 React 函數的最頂層以及任何 return 之前調用他們。
解決這個問題最直接的辦法就是按照官方文檔所說的,確定總是在你的 React 函數的最頂層以及任何 return 之前調用他們:
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 (
<div>
<div>Name: {game.name}</div>
<div>Description: {game.description}</div>
</div>
);
}
這樣,無論傳入的id是否為空,useState和useEffect總會以相同的順序來調用,這樣就不會出錯啦~
React官方文檔中的Hook規則:《Hook 規則》,可以使用插件eslint-plugin-react-hooks來幫助我們檢查這些規則。
2. 不要使用舊的狀态
先來看一個計數器的例子:
const Increaser = () => {
const [count, setCount] = useState(0);
const increase = useCallback(() => {
setCount(count + 1);
}, [count]);
const handleClick = () => {
increase();
increase();
increase();
};
return (
<>
<button onClick={handleClick}>+</button>
<div>Counter: {count}</div>
</>
);
}
這裡的handleClick方法會在點選按鈕後執行三次增加狀态變量count的操作。那麼點選一次是否會增加3呢?事實并非如此。點選按鈕之後,count隻會增加1。問題就在于,當我們點選按鈕時,相當于下面的操作:
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
當第一次調用setCount(count + 1)時是沒有問題的,它會将count更新為1。接下來第2、3次調用setCount時,count還是使用了舊的狀态(count為0),是以也會計算出count為1。發生這種情況的原因就是狀态變量會在下一次渲染才更新。
解決這個問題的辦法就是,使用函數的方式來更新狀态:
const Increaser = () => {
const [count, setCount] = useState(0);
const increase = useCallback(() => {
setCount(count => count + 1);
}, [count]);
const handleClick = () => {
increase();
increase();
increase();
};
return (
<>
<button onClick={handleClick}>+</button>
<div>Counter: {count}</div>
</>
);
}
這樣改完之後,React就能拿到最新的值,當點選按鈕時,就會每次增加3。是以需要記住:如果要使用目前狀态來計算下一個狀态,就要使用函數的式方式來更新狀态:
setValue(prevValue => prevValue + someResult)
3. 不要建立舊的閉包
衆所周知,React Hooks是依賴閉包實作的。當使用接收一個回調作為參數的鈎子時,比如:
useEffect(callback, deps)
useCallback(callback, deps)
此時,我們就可能會建立一個舊的閉包,該閉包會捕獲過時的狀态或者prop變量。這麼說可能有些抽象,下面來看一個例子,這個例子中,useEffect每2秒會列印一次count的值:
const WatchCount = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(function log() {
console.log(`Count: ${count}`);
}, 2000);
}, []);
const handleClick = () => setCount(count => count + 1);
return (
<>
<button onClick={handleClick}>+</button>
<div>Count: {count}</div>
</>
);
}
最終的輸出的結果如下:
可以看到,每次列印的count值都是0,和實際的count值并不一樣。為什麼會這樣呢?
在第一次渲染時應該沒啥問題,閉包log會将count列印出0。從第二次開始,每次當點選按鈕時,count會增加1,但是setInterval仍然調用的是從初次渲染中捕獲的count為0的舊的log閉包。log方法就是一個舊的閉包,因為它捕獲的是一個過時的狀态變量count。
這裡的解決方案就是,當count發生變化時,就重置定時器:
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 (
<>
<button onClick={handleClick}>+</button>
<div>Count: {count}</div>
</>
);
}
這樣,當狀态變量count發生變化時,就會更新閉包。為了防止閉包捕獲到舊值,就要確定在提供給hook的回調中使用的prop或者state都被指定為依賴性。
4. 不要忘記清理副作用
有很多副作用,比如fetch請求、setTimeout等都是異步的,如果不需要這些副作用或者元件在解除安裝時,不要忘記清理這些副作用。下面來看一個計數器的例子:
const DelayedIncreaser = () => {
const [count, setCount] = useState(0);
const [increase, setShouldIncrease] = useState(false);
useEffect(() => {
if (increase) {
setInterval(() => {
setCount(count => count + 1)
}, 1000);
}
}, [increase]);
return (
<>
<button onClick={() => setShouldIncrease(true)}>
+
</button>
<div>Count: {count}</div>
</>
);
}
const MyApp = () => {
const [show, setShow] = useState(true);
return (
<>
{show ? <DelayedIncreaser /> : null}
<button onClick={() => setShow(false)}>解除安裝</button>
</>
);
}
這個元件很簡單,就是在點選按鈕時,狀态變量count每秒會增加1。當我們點選+按鈕時,它會和我們預期的一樣。但是當我們點選“解除安裝”按鈕時,控制台就會出現警告:
修複這個問題隻需要使用useEffect來清理定時器即可:
useEffect(() => {
if (increase) {
const id = setInterval(() => {
setCount(count => count + 1)
}, 1000);
return () => clearInterval(id);
}
}, [increase]);
當我們編寫一些副作用時,我們需要知道這個副作用是否需要清除。
5. 不要在不需要重新渲染時使用useState
在React hooks 中,我們可以使用useState hook來進行狀态的管理。雖然使用起來比較簡單,但是如果使用不恰當,就可能會出現意想不到的問題。來看下面的例子:
const Counter = () => {
const [counter, setCounter] = useState(0);
const onClickCounter = () => {
setCounter(counter => counter + 1);
};
const onClickCounterRequest = () => {
apiCall(counter);
};
return (
<div>
<button onClick={onClickCounter}>Counter</button>
<button onClick={onClickCounterRequest}>Counter Request</button>
</div>
);
}
在上面的元件中,有兩個按鈕,第一個按鈕會觸發計數器加一,第二個按鈕會根據目前的計數器狀态發送一個請求。可以看到,狀态變量counter并沒有在渲染階段使用。是以,每次點選第一個按鈕時,都會有不需要的重新渲染。
是以,當遇到這種需要在元件中使用一個變量在渲染中保持其狀态,并且不會觸發重新渲染時,那麼useRef會是一個更好的選擇,下面來對上面的例子使用useRef進行改編:
const Counter = () => {
const counter = useRef(0);
const onClickCounter = () => {
counter.current++;
};
const onClickCounterRequest = () => {
apiCall(counter.current);
};
return (
<div>
<button onClick={onClickCounter}>Counter</button>
<button onClick={onClickCounterRequest}>Counter Request</button>
</div>
);
}
6. 不要缺少useEffect依賴
useEffect是React Hooks中最常用的Hook之一。預設情況下,它總是在每次重新渲染時運作。但這樣就可能會導緻不必要的渲染。我們可以通過給useEffect設定依賴數組來避免這些不必要的渲染。
來看下面的例子:
const Counter = () => {
const [count, setCount] = useState(0);
const showCount = (count) => {
console.log("Count", count);
};
useEffect(() => {
showCount(count);
}, []);
return (
<div>Counter: {count}</div>
);
}
這個元件可能沒有什麼實際的意義,隻是列印了count的值。這時就會有一個警告:
這裡是說,useEffect缺少一個count依賴,這樣是不安全的。我們需要包含一個依賴項或者移除依賴數組。否則useEffect中的代碼可能會使用舊的值。
const Counter = () => {
const [count, setCount] = useState(0);
const showCount = (count) => {
console.log("Count", count);
};
useEffect(() => {
showCount(count);
}, [count]);
return (
<div>Counter: {count}</div>
);
}
如果useEffect中沒有用到狀态變量count,那麼依賴項為空也會是安全的:
useEffect(() => {
showCount(996);
}, []);
今天的分享就到這裡,如果覺得有用就來個三連吧~