總覽
產生"Too many re-renders. React limits the number of renders to prevent an infinite loop"錯誤有多方面的原因:
- 在一個元件的渲染方法中呼叫一個設定狀態的函式。
- 立即呼叫一個事件處理器,而不是傳遞一個函式。
- 有一個無限設定與重渲染的
useEffect
鉤子。
這裡有個示例來展示錯誤是如何發生的:
import {useState} from 'react';
export default function App() {
const [counter, setCounter] = useState(0);
// ⛔️ Too many re-renders. React limits the number
// of renders to prevent an infinite loop.
return (
<div>
<button onClick={setCounter(counter + 1)}>Increment</button>
<h1>Count: {counter}</h1>
</div>
);
}
上述程式碼問題在於,我們在onClick
事件處理器中立即呼叫了setCounter
函式。
該函式是在頁面載入時立即被呼叫,而不是事件觸發後呼叫。
傳遞函式
為了解決該錯誤,為onClick
事件處理器傳遞函式,而不是傳遞呼叫函式的結果。
import {useState} from 'react';
export default function App() {
const [counter, setCounter] = useState(0);
return (
<div>
<button onClick={() => setCounter(counter + 1)}>Increment</button>
<h1>Count: {counter}</h1>
</div>
);
}
現在,我們為事件處理器傳遞了函式,而不是當頁面載入時呼叫setCounter
方法。
如果該方法在頁面載入時被呼叫,就會觸發一個
setState
動作,元件就會無限重新渲染。
如果我們試圖立即設定一個元件的狀態,而不使用一個條件或事件處理器,也會發生這個錯誤。
import {useState} from 'react';
export default function App() {
const [counter, setCounter] = useState(0);
// ⛔️ Too many re-renders. React limits the number
// of renders to prevent an infinite loop.
setCounter(counter + 1);
return (
<div>
<h1>Count: {counter}</h1>
</div>
);
}
問題在於,setCounter
函式在元件渲染時被呼叫、更新狀態,並導致重新渲染,而且是無限重新渲染。
你可以透過向useState()
鉤子傳遞一個初始值或一個函式來初始化狀態,從而解決這個錯誤。
import {useState} from 'react';
export default function App() {
const [counter, setCounter] = useState(() => 100 + 100);
return (
<div>
<h1>Count: {counter}</h1>
</div>
);
}
我們向useState
方法傳遞了一個函式。這個函式只會在元件第一次渲染時被呼叫,並且會計算出初始狀態。你也可以直接向useState
方法傳遞一個初始值。
另外,你也可以像前面的例子那樣使用一個條件或事件處理器。
import {useState} from 'react';
export default function App() {
const [counter, setCounter] = useState(0);
// ?️ your condition here
if (Math.random() > 0.5) {
setCounter(counter + 1);
}
return (
<div>
<h1>Count: {counter}</h1>
</div>
);
}
如果你像上面的例子那樣使用一個條件,請確保該條件不總是返回一個真值,因為這將導致無限的重新渲染迴圈。
"Too many re-renders. React limits the number of renders to prevent an infinite loop"錯誤也會在使用useEffect
方法時發生,該方法的依賴會導致無限重新渲染。
import {useEffect, useState} from 'react';
export default function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
// ⛔️ Too many re-renders. React limits the number
// of renders to prevent an infinite loop.
setCounter(counter + 1);
}); // ?️ forgot to pass dependency array
return (
<div>
<h1>Count: {counter}</h1>
</div>
);
}
上述程式碼問題在於,我們沒有為useEffect
鉤子傳遞依賴陣列。
這意味著該鉤子會在每次渲染時執行,它會更新元件的狀態,然後無限重新執行。
傳遞依賴
解決該錯誤的一種辦法是,為useEffect
提供空陣列作為第二個引數。
import {useEffect, useState} from 'react';
export default function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
setCounter(counter + 1);
}, []); // ?️ empty dependencies array
return (
<div>
<h1>Count: {counter}</h1>
</div>
);
}
如果你為
useEffect
方法傳遞空陣列依賴作為第二個引數,該方法只在元件的初始渲染時執行。
該程式碼將計數器遞增到1
,並且不再執行,無論App
元件是否被重新渲染。
如果你必須指定一個依賴來無限地重新渲染你的元件,試著尋找一個可以防止這種情況的條件。
import {useEffect, useState} from 'react';
export default function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
// ?️ some condition here
if (Math.random() > 0.5) {
setCounter(counter + 1);
}
}, [counter]);
return (
<div>
<h1>Count: {counter}</h1>
</div>
);
}
有可能是某些邏輯決定了狀態是否應該被更新,而狀態不應該在每次重新渲染時被設定。
確保你沒有使用一個在每次渲染時都不同的物件或陣列作為useEffect
鉤子的依賴。
import {useEffect, useState} from 'react';
export default function App() {
const [address, setAddress] = useState({country: '', city: ''});
const obj = {country: 'Chile', city: 'Santiago'};
useEffect(() => {
// ⛔️ Too many re-renders. React limits the number
// of renders to prevent an infinite loop.
setAddress(obj);
console.log('useEffect called');
}, [obj]);
return (
<div>
<h1>Country: {address.country}</h1>
<h1>City: {address.city}</h1>
</div>
);
}
問題在於,在JavaScript中,物件是透過引用進行比較的。obj
變數儲存了一個具有相同鍵值對的物件,但每次渲染時的引用不同(在記憶體中的位置不同)。
移入依賴
解決該錯誤的一種辦法是,把這個物件移到useEffect
鉤子裡面,這樣我們就可以把它從依賴陣列中移除。
import {useEffect, useState} from 'react';
export default function App() {
const [address, setAddress] = useState({country: '', city: ''});
useEffect(() => {
// ?️ move object inside of useEffect
// and remove it from dependencies array
const obj = {country: 'Chile', city: 'Santiago'};
setAddress(obj);
console.log('useEffect called');
}, []);
return (
<div>
<h1>Country: {address.country}</h1>
<h1>City: {address.city}</h1>
</div>
);
}
傳遞物件屬性
另一個解決方案是將物件的屬性傳遞給依賴陣列。
import {useEffect, useState} from 'react';
export default function App() {
const [address, setAddress] = useState({country: '', city: ''});
const obj = {country: 'Chile', city: 'Santiago'};
useEffect(() => {
setAddress({country: obj.country, city: obj.city});
console.log('useEffect called');
// ?️ object properties instead of the object itself
}, [obj.country, obj.city]);
return (
<div>
<h1>Country: {address.country}</h1>
<h1>City: {address.city}</h1>
</div>
);
}
現在React不是在測試一個物件是否發生了變化,而是在測試obj.country
和obj.city
字串在渲染之間是否發生了變化。
記憶值
另外,我們可以使用useMemo
鉤子來獲得一個在不同渲染之間不會改變的記憶值。
import {useEffect, useMemo, useState} from 'react';
export default function App() {
const [address, setAddress] = useState({country: '', city: ''});
// ?️ get memoized value
const obj = useMemo(() => {
return {country: 'Chile', city: 'Santiago'};
}, []);
useEffect(() => {
setAddress(obj);
console.log('useEffect called');
}, [obj]);
return (
<div>
<h1>Country: {address.country}</h1>
<h1>City: {address.city}</h1>
</div>
);
}
我們將物件的初始化包裹在
useMemo
鉤子裡面,以獲得一個不會在渲染之間改變的記憶值。
我們傳遞給useMemo
鉤子的第二個引數是一個依賴陣列,它決定了我們傳遞給useMemo
的回撥函式何時被重新執行。
需要注意的是,陣列在JavaScript中也是透過引用進行比較的。所以一個具有相同值的陣列也可能導致你的useEffect
鉤子被無限次觸發。
import {useEffect, useMemo, useState} from 'react';
export default function App() {
const [nums, setNums] = useState([1, 2, 3]);
const arr = [4, 5, 6];
useEffect(() => {
// ⛔️ Too many re-renders. React limits the number
// of renders to prevent an infinite loop.
setNums(arr);
console.log('useEffect called');
}, [arr]);
return <div>{nums[0]}</div>;
}
陣列在重新渲染之間儲存相同的值,但指向記憶體中的不同位置,並且在每次元件重新渲染時有不同的引用。
在處理陣列時,我們用於物件的方法同樣有效。例如,我們可以使用useMemo
鉤子來獲得一個在渲染之間不會改變的記憶值。
import {useEffect, useMemo, useState} from 'react';
export default function App() {
const [nums, setNums] = useState([1, 2, 3]);
const arr = useMemo(() => {
return [4, 5, 6];
}, []);
useEffect(() => {
setNums(arr);
console.log('useEffect called');
}, [arr]);
return <div>{nums[0]}</div>;
}
我們將陣列的初始化包裹在useMemo
鉤子裡面,以獲得一個不會在不同渲染之間改變的記憶值。