知識閱讀 - 5 種 useEffect 無限迴圈的模式
在 React 16 發佈後,useEffect
可以說是目前使用最多的 hook,因為它提供了componentDidMount
、componentDidUpdate
和 componentWillUnmount
生命週期的組合功能。
// callback
// ||
// V
useEffect(()=>{}, [])
// ^
// ||
// dependency
當 dependency 改變時 useEffect 才會去 trigger callback 函數。這邊要注意的就是 dependency。因為當你沒有給予 dependency,然後又在 useEffect 裡面去更新 state 的話,就會造成無限迴圈了。
useEffect(() => {
fetch("/api/user")
.then((res) => res.json)
.then((res) => {
setData(res);
});
});
永遠要記得這句話「當 state 或 props 改變時,元件就會重新渲染」,所以當上面的程式碼中的 setData(res)
就會去更新 state,然後就會觸發重新渲染,但是 useEffect
沒有值作為 dependency,就會重新被執行,結果又會重新的更新 setData(res)
,一直無限的重新渲染。
如何修正
修正的辦法可以改成以下:
useEffect(() => {
fetch("/api/user")
.then((res) => res.json)
.then((res) => {
setData(res);
});
}, []); // <--- dependency
但某些情境下,官方上會認為是不安全的做法
函數作為 dependency
useEffect
使用 shallow object 來判斷是否值有改變與否,但是因為 JavaScript 本身詭異的比較系統,會有以下的問題
var mark1 = function(){ return ('100')}; // has a unique object reference
var mark2 = function(){ return('100')}; // has a unique object reference
mark1 == mark2; // false
mark1 === mark2; // false
然後我們來看看以下的範例
import React, { useCallback, useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const getData = () => {
return window.localStorage.getItem("token");
};
const [dep, setDep] = useState(getData());
useEffect(() => {
setCount(count + 1);
}, [getData]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={() => setCount(count + 1)}>{count}</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
我們將 getData
這個函數拿來當作 dependency,但是執行之後你會發現出現錯誤 Maximum update depth exceeded
。這就是因為在 JavaScript 的系統中,getData
永遠都會被判斷為不相同造成的。
如何修正
這邊可以使用 useCallback
來修正此問題,因為 useCallback
會回傳一個 memoized 版本,只有在 dependency 改變時才改變
const getData = useCallback(() => {
return window.localStorage.getItem("token");
}, []); // <- dependencies
陣列作為 dependency
因為在 shallow comparison 中陣列總是錯誤的 ([] === [] // false
),所以也會造成無限迴圈
import React, { useCallback, useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const dep = ['a'];
const [value, setValue] = useState(['b']);
useEffect(() => {
setValue(['c']);
}, [dep]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={() => setCount(count + 1)}>{count}</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
How to fix
因為 useCallback
會回傳函數,所以我們無法使用它來修正。這邊我們使用 useRef
。useRef
回傳一個可變物件,其 .current
具有初始值。
import React, { useEffect, useState, useRef } from 'react';
export default function Home() {
const [value, setValue] = useState(['b']);
const {current:a} = useRef(['a'])
useEffect(() => {
setValue(['c']);
}, [a])
}
物件作為 dependency
物件跟陣列一樣的關係,所以也都會造成無限迴圈
import React, { useCallback, useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const data = {
is_fetched:false
};
useEffect(() => {
setCount(count + 1);
}, [data]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={() => setCount(count + 1)}>{count}</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
How to fix
這邊我們使用 useMemo
來修正這問題。useMemo
只會在 dependency 發生變化時重新計算 memoized value。
import React, { useMemo, useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const data = useMemo(()=>({
is_fetched:false
}), []); // <- dependencies
useEffect(() => {
setCount(count + 1);
}, [data]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={() => setCount(count + 1)}>{count}</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}