React報錯之Too many re-renders

chuckQu發表於2022-12-13

總覽

產生"Too many re-renders. React limits the number of renders to prevent an infinite loop"錯誤有多方面的原因:

  1. 在一個元件的渲染方法中呼叫一個設定狀態的函式。
  2. 立即呼叫一個事件處理器,而不是傳遞一個函式。
  3. 有一個無限設定與重渲染的useEffect鉤子。

too-many-re-renders-react-limits-the-number.png

這裡有個示例來展示錯誤是如何發生的:

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.countryobj.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鉤子裡面,以獲得一個不會在不同渲染之間改變的記憶值。

相關文章