知識閱讀 - 5 種 useEffect 無限迴圈的模式

在 React 16 發佈後,useEffect 可以說是目前使用最多的 hook,因為它提供了componentDidMountcomponentDidUpdatecomponentWillUnmount 生命週期的組合功能。

//          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 會回傳函數,所以我們無法使用它來修正。這邊我們使用 useRefuseRef 回傳一個可變物件,其 .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>
  );
}

Reference

Did you find this article valuable?

Support 攻城獅 by becoming a sponsor. Any amount is appreciated!