你不知道 useEffect

2025-05-28

你不知道 useEffect

useEffect 是每个 ReactJS 开发者都应该了解的最常见的 React Hook 之一。但正确使用 useEffect 并不像你想象的那么容易。今天,我将回顾一些我认为很多新手在使用 useEffect 时容易犯的错误,并给出相应的解决方案。

1. 快速概览

我认为我们应该先了解一下 useEffect 的一些基础知识。我们都知道 useEffect 是一个 React Hook,用于处理副作用函数(对于那些不知道副作用函数是什么的人来说——它只是一个与外界交互的函数。例如:在屏幕上记录一些内容、创建文件、将数据保存到数据库、更改 DOM ……)。

如果你了解 React 的生命周期,那么 useEffect 会完成 componentDidMount、componentDidUpdate 和 componentWillUnmount 的工作。没错,一个钩子里包含了三个方法。因此,useEffect 的用例就是上述方法的用例:

  • 调用 API
  • 当状态/属性改变时执行某些操作
  • 卸载时/下次渲染前清理内容
  • 不仅如此……

语法: useEffect 的语法非常简单:

useEffect(someFunc, [deps_array]);
Enter fullscreen mode Exit fullscreen mode

第一个参数将是一个副作用函数。

第二个参数将是一个依赖项数组,它决定 useEffect 是否运行。

2. 深入研究 useEffect。

a. 使用形式效果

首先,我们来谈谈 useEffect 的三种形式。我不知道称之为“形式”是否合适,但至少对我来说是合理的(希望你们也能理解!)

useEffect 的形式由第二个参数决定:依赖项数组。

首先,deps_arrray 是可选的,你不必传递第二个参数。如果只传递第一个参数,我们可以使用 useEffect 的第一个形式。

useEffect(func);
Enter fullscreen mode Exit fullscreen mode

在这种情况下,传递给 useEffect 的函数将在组件的每次渲染时运行。当你需要在组件的每次渲染时执行某些操作时,可以使用它。但是,如果你不想造成无限渲染或内存泄漏,则在使用这种形式的 useEffect 时应谨慎。你应该尽可能避免使用这种形式的 useEffect。

例如

const App = () => {
    useEffect(() => {
        console.log("This effect is called on every render");
    });

    // return..
}
Enter fullscreen mode Exit fullscreen mode

每次重新渲染组件时,您都会看到该日志。

如果传递一个空数组作为 useEffect 的第二个参数,您将获得它的第二种形式。

useEffect(func, []);
Enter fullscreen mode Exit fullscreen mode

与第一种形式相反,传递给 useEffect 的函数只会运行一次(第一次渲染之后)。

例如:

const App = () => {
    useEffect(() => {
        console.log("Effect has been called");
    }, []);

    // return...
}
Enter fullscreen mode Exit fullscreen mode

除了第一次渲染之外,您将不会再看到“效果已被调用”的日志。

useEffect 的第三种形式是传递依赖项数组中带有一些变量的数组

useEffect(func, [variableA, varibleB,...]);
Enter fullscreen mode Exit fullscreen mode

这一次,每次依赖项数组中的任何元素发生变化时,都会运行 func。

例如:

const App = () => {
    const [counter, setCounter] = useState(0);
    useEffect(() => {
        // This will run every time counter changed
        console.log('counter: ', counter);
    }, [counter]);

    // return
}
Enter fullscreen mode Exit fullscreen mode

⚠️ 需要注意的是:无论你是否传递了依赖项数组,而只是打算在某个依赖项发生变化时运行 useEffect 的第三种形式的函数,useEffect 总会在组件第一次挂载时运行。

例如:

const App = () => {
  const [a, setA] = useState(0);
  const [b, setB] = useState(1);

  useEffect(() => {
    console.log("run only once");
  }, []);

  useEffect(() => {
    console.log("Change a");
  }, [a]);

  useEffect(() => {
    console.log("Change b");
  }, [b]);

  return (
       ...
  );
}
Enter fullscreen mode Exit fullscreen mode

在第一次渲染时,您将看到三个日志:

run only once
change a
change b
Enter fullscreen mode Exit fullscreen mode

因此,即使 a 和 b 完全没有改变,与这些变量关联的 useEffect 仍然会在首次渲染时运行。如果你有多个 useEffect 触发了一些繁重的副作用(例如 API 调用),那么这将是一个大问题。例如,你需要渲染一个包含分页和搜索查询的列表。

import { useEffect, useState } from "react";
import "./styles.css";

const App = () => {
  const [query, setQuery] = useState(0);
  const [page, setPage] = useState(1);

  useEffect(() => {
    console.log("call api first time");
  }, []);

  useEffect(() => {
    console.log("Call api when query changes");
  }, [query]);

  useEffect(() => {
    console.log("Call api when page changes");
  }, [page]);

  return (
   ...
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

第一次安装组件时,您将看到三个日志:

call api first time
call api when query changes
call api when page changes
Enter fullscreen mode Exit fullscreen mode

让我们想象一下,如果您监听许多其他字段的变化,并且在这些字段的每个 useEffect 上触发 API 调用(或任何其他副作用函数),那么在第一次呈现您的应用程序时,将触发许多不必要的 API 调用,这可能会影响应用程序的性能并导致一些您可能不会想到的错误(如果您真的不需要触发所有 API 调用或所有 useEffect 的副作用函数)

解决这个问题的方法有很多,但我先介绍一种常用的方法——也是我最喜欢的一种。你可以创建一个变量来检查组件是否已挂载。

const App = () => {
  const [query, setQuery] = useState(0);
  const [page, setPage] = useState(1);
  const isMounted = useRef(false);

  useEffect(() => {
    if (isMounted.current) {
      console.log("Call api when query changes");
    }
  }, [query]);

  useEffect(() => {
    if (isMounted.current) {
      console.log("Call api when page changes");
    }
  }, [page]);

  useEffect(() => {
    console.log("call api first time");
    isMounted.current = true;
  }, []);

  return (
    ...
  );
};
Enter fullscreen mode Exit fullscreen mode

这是第一次安装的结果

call api first time
Enter fullscreen mode Exit fullscreen mode

还要注意 useEffect 的顺序,我这样安排是有原因的。为了使这个方案有效,你必须将保存第一次渲染/挂载(或任何你想叫它的名字)值的变量放在最后一个 useEffect 中。React 会按以下顺序执行 useEffects:

b. 依赖关系

在上一节中,我提到了传递给 useEffect 的依赖项列表,通过这样做,您可以“监听”依赖项列表中每个元素的任何变化。

这里的问题是:大多数情况下,你会使用对象和函数,如果你将对象/函数类型的变量传递给依赖列表,有时你的程序可能无法按预期运行。让我们考虑以下示例:

import { memo, useState } from "react";
const List = memo((list) => {
  useEffect(() => {
    console.log("list changed");
  }, [list]);

  return <ul>{list?.length > 0 && list.map((e) => <li>{e}</li>)}</ul>;
});

const App = () => {
  const [a, setA] = useState(0);

  const someFunc = () => console.log("This is a random function");

  useEffect(() => {
    console.log("Use effect of someFunc's called");
  }, [someFunc]);

  const fakeList = () => ["number 1", "number 2"];

  return (
    <div className="App">
      <h1>Variable a: {a} </h1>
      <button onClick={() => setA((v) => v + 1)}>Increase a</button>
      <button onClick={someFunc}>call someFunc()</button>
      <List list={fakeList} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

不,尝试点击“增加”按钮,

我们将得到这个(不是在第一次渲染中)

list changed
Use effect of someFunc's called
Enter fullscreen mode Exit fullscreen mode

每次我们点击“增加 a”按钮,即使我们并没有修改 someFunc 和 fakeList 的值,监听 someFunc 和 list 变化的 useEffect 都会被触发(注意,我用 memo 包裹了 List 组件,以防止它在 props - list 发生变化时重新渲染)。这是因为在比较对象/函数时,React 会比较它们的引用。所以,当点击按钮“增加 a”时,App 组件会重新渲染(由于状态变化)→someFunc 和 fakeList 会更新,所以在每次渲染时,someFunc 和 fakeList 都会有新的引用,因此,React 会标记 someFunc 和 fakeList 已更改,并运行与它们关联的 useEffect。你应该注意这一点,以防止不必要的重新渲染和不必要的 useEffect 触发。

正如我之前提到的,React 会通过引用来比较对象/函数。处理对象/函数类型的依赖时,有两种常见情况需要注意:

  • 情况 1:对象/函数相同,但引用不同(我们示例中的情况)。
  • 情况 2:对象具有不同的值,但它们的引用是相同的(当您部分更新对象但不触发重新更新操作时会发生这种情况)。

以上两种情况都会影响我们的 useEffect,从而导致意外行为。

有很多解决方案可以避免这些情况,我将向大家介绍我通常使用的方法。

对于第一种情况:记忆化。

是的,为了做到这一点,我们将提出 2 个新的钩子(也许你们之前听说过:useCallback 和 useMemo)。

为了快速参考,你们可以在这里看到这些钩子的区别:useCallback 和 useMemo 之间的区别,或者在官方网站上阅读详细信息:useCallbackuseMemo

稍微改变一下我们的代码

import { memo, useCallback, useEffect, useMemo, useState } from "react";

const List = memo((list) => {
  useEffect(() => {
    console.log("list changed");
  }, [list]);

  return <ul>{list?.length > 0 && list.map((e) => <li>{e}</li>)}</ul>;
});

const App = () => {
  const [a, setA] = useState(0);

  const someFunc = useCallback(
    () => console.log("This is a random function"),
    []
  );

  useEffect(() => {
    console.log("Use effect of someFunc's called");
  }, [someFunc]);

  const fakeList = useMemo(() => ["number 1", "number 2"], []);

  return (
    <div className="App">
      <h1>Variable a: {a} </h1>
      <button onClick={() => setA((v) => v + 1)}>Increase a</button>
      <button onClick={someFunc}>call someFunc()</button>
      <List list={fakeList} />
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

我用 useCallback 包装了 someFunc (实际上,如果你将 someFunc 作为 useEffect 的依赖项之一,而没有用 useCallback 包装它,如果你的 IDE/文本编辑器集成了 ESLint,你会收到这样的警告:“someFunc”函数使得 useEffect Hook(第 19 行)的依赖项在每次渲染时都会发生变化。要解决这个问题,请将“someFunc”的定义包装在其 useCallback() Hook 中),并用 useMemo 包装了我们的 fakeList 。出于学习目的,我们暂时将 useCallback 和 useMemo 的依赖项列表留空,但在实际项目中使用这些 Hook 时,应该小心它们的依赖项列表。

现在,如果我们运行程序并点击增加按钮,我们将不再看到来自 someFunc 和 list 的 useEffect 的日志(第一次渲染除外)。

⚠️ 代码中的每一行都是有成本的!useCallback 和 useMemo 会消耗程序的内存(因为它需要将值存储在某处),因此在使用这些钩子时应小心,仅在真正必要时才使用它们。

对于第二种情况,我不会举例说明,因为解决这个问题的方法是简单地监听属性而不是对象。

但是 useEffect 依赖列表的最佳实践是,您应该始终尽可能处理原始类型,以避免出现意外结果。

此部分的源代码可以在这里找到:https://codesandbox.io/s/hopeful-cherry-md0db? file=/src/App.js:356-388

c.清理功能

在概述部分,我说过 useEffect 可以完成 componenWillUnmount 生命周期的工作。它是 useEffect 中的返回函数

useEffect(() => {
    // do something
    return () => {
        // do cleanup stu
    }
}, []);
Enter fullscreen mode Exit fullscreen mode

返回函数将在下次调用 useEffect 函数之前执行“清理”操作。

因此,在上面的例子中,它相当于在 componentWillUnmount 中执行一些代码,因为上面例子中 useEffect 的形式是 #2,它只在组件第一次渲染后运行一次。

我知道这有点抽象。所以我们会通过一些例子来解释,希望大家看完之后能理解。

const List = () => {
  useEffect(() => {
    console.log("first render list");

    return () => console.log("unmount list");
  }, []);

  return <h1>This is a list</h1>;
};

const App = () => {
  const [isListVisible, setIsListVisible] = useState(true);

  useEffect(() => {
    return () => console.log("clean up on change isListVisible");
  }, [isListVisible]);

  return (
    <div className="App">
      <button onClick={() => setIsListVisible((v) => !v)}>Toggle List</button>
      {isListVisible && <List />}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

每次单击“切换列表”时,您都会看到两个日志:一个来自列表中表单#2的useEffect,另一个来自#3的useEffect监听isListVisible的变化。

列表可见

那么为什么清理是必要的呢?让我们考虑下面的例子:

让我们稍微改变一下上面的例子:

const List = () => {
  useEffect(() => {
    setInterval(() => console.log("interval from list"), 1000);
    return () => console.log("unmount list");
  }, []);

  return <h1>This is a list</h1>;
};

const App = () => {
  const [isListVisible, setIsListVisible] = useState(true);

  useEffect(() => {
    return () => console.log("clean up on change isListVisible");
  }, [isListVisible]);

  return (
    <div className="App">
      <button onClick={() => setIsListVisible((v) => !v)}>Toggle List</button>
      {isListVisible && <List />}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

我给 List 添加了 setInterval 函数,它会每 1 秒记录一次。但关键在于:即使 List 被卸载,这个间隔函数仍然会运行。

设置间隔图像

所以,即使组件被卸载了,我们施加到该组件上的一些副作用仍然会运行。在我们的例子中,它只是一个时间间隔,但在现实生活中,如果是一堆 API 调用,一堆其他副作用,想象一下,即使它们的组件被卸载了,它们仍然会运行,那么这可能是一个影响我们应用性能的黑洞。

在我们的示例中,为了解决这个问题,我们可以简单地将 clearInterval 添加到我们的清理函数中:

const List = () => {
  useEffect(() => {
    const listInterval = setInterval(
      () => console.log("interval from list"),
      1000
    );
    return () => {
      console.log("unmount list");
      clearInterval(listInterval);
    };
  }, []);

  return <h1>This is a list</h1>;
};
Enter fullscreen mode Exit fullscreen mode

我知道如果你是新手,有时你不会注意到清理的东西,但它们确实是必要的,你应该花时间考虑它们。

此部分的代码可以在这里找到:https://codesandbox.io/s/flamboyant-andras-xo86e? file=/src/App.js:69-357

3.总结

好了,我已经深入讲解了 useEffect 的一些内容。除了我提到的注意事项之外,在使用 useEffect 时,你还需要注意更多场景,才能让你的应用以最佳状态运行。所以,请继续学习,如果你有任何问题或更正,请留言,我会仔细查看。谢谢。再见。😈

文章来源:https://dev.to/trunghieu99tt/you-don-t-know-useeffect-4j9h
PREV
使用 Kotlin 在 Android 上构建 WhatsApp 克隆版 – 第 1 部分
NEXT
Typescript 设计模式