How to useEffect in React【译】

376 阅读6分钟

原文链接www.robinwieruch.de/react-useef…

在本教程中,您将了解有关 React 的 useEffect Hook 的所有内容。 假设我们有这两个组件,父组件使用 React 的 useState Hook 管理状态,其子组件使用状态并使用回调事件处理程序修改状态:

import * as React from 'react';
 
const App = () => {
  const [toggle, setToggle] = React.useState(true);
 
  const handleToggle = () => {
    setToggle(!toggle);
  };
 
  return <Toggler toggle={toggle} onToggle={handleToggle} />;
};
 
const Toggler = ({ toggle, onToggle }) => {
  return (
    <div>
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
 
      {toggle && <div>Hello React</div>}
    </div>
  );
};
 
export default App;

基于来自父组件的有状态布尔标志,子组件有条件地呈现“Hello React”。 现在让我们深入研究 React 的 useEffect Hook。 本质上,只要你想运行 useEffect 就会运行一个副作用函数。 它只能在组件挂载时、组件渲染时或仅在组件重新渲染时运行,等等。 我们将通过各种 useEffect 示例来演示其用法。

REACT USEEFFECT HOOK: ALWAYS

让我们看看 React 的 useEffect Hook 的第一个例子,我们将副作用函数作为参数传入:

const Toggler = ({ toggle, onToggle }) => {
  React.useEffect(() => {
    console.log('I run on every render: mount + update.');
  });
 
  return (
    <div>
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
 
      {toggle && <div>Hello React</div>}
    </div>
  );
};

这是 useEffect 最直接的用法,我们只传递一个参数——一个函数。 这个函数将在每次渲染时渲染——意味着它在组件的第一次渲染上运行(也称为组件的挂载或安装)和组件的每次重新渲染(也称为组件的更新或更新) .

REACT USEEFFECT HOOK: MOUNT

如果你只想在组件的第一次渲染上运行 React 的 useEffect Hook(也只在挂载时调用),那么你可以将第二个参数传递给 useEffect:

const Toggler = ({ toggle, onToggle }) => {
  React.useEffect(() => {
    console.log('I run only on the first render: mount.');
  }, []);
 
  return (
    <div>
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
 
      {toggle && <div>Hello React</div>}
    </div>
  );
}

第二个参数——这里是一个空数组——被称为依赖数组。 如果依赖数组为空,则 React 的 useEffect Hook 中使用的副作用函数没有依赖关系,这意味着它只在组件第一次渲染时运行。

REACT USEEFFECT HOOK: UPDATE

之前你已经了解了 React 的 useEffect Hook 的依赖数组。 仅当某个变量发生变化时,此数组才可用于运行 useEffect 的副作用函数:

const Toggler = ({ toggle, onToggle }) => {
  React.useEffect(() => {
    console.log('I run only if toggle changes (and on mount).');
  }, [toggle]);
 
  return (
    <div>
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
 
      {toggle && <div>Hello React</div>}
    </div>
  );
};

现在这个 React 组件的副作用函数只有在依赖数组中的变量发生变化时才会运行。 但是,请注意该函数也在组件的第一次渲染(挂载)上运行。 无论如何,依赖数组的大小可以增长,因为它毕竟是一个数组,因此您可以传入多个变量。 让我们在组件中添加以下内容来检查一下:

const Toggler = ({ toggle, onToggle }) => {
  const [title, setTitle] = React.useState('Hello React');
 
  React.useEffect(() => {
    console.log('I still run only if toggle changes (and on mount).');
  }, [toggle]);
 
  const handleChange = (event) => {
    setTitle(event.target.value);
  };
 
  return (
    <div>
      <input type="text" value={title} onChange={handleChange} />
 
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
 
      {toggle && <div>{title}</div>}
    </div>
  );
};

React 的 useEffect Hook 中的副作用函数仍然只有在依赖数组中的一个变量发生变化时才会运行。 即使我们在 input 元素中输入内容时组件都会更新,但 useEffect 不会在此更新上运行。 只有当我们在依赖数组中提供新变量时,副作用函数才会为两个更新运行:

const Toggler = ({ toggle, onToggle }) => {
  const [title, setTitle] = React.useState('Hello React');
 
  React.useEffect(() => {
    console.log('I run if toggle or title change (and on mount).');
  }, [toggle, title]);
 
  const handleChange = (event) => {
    setTitle(event.target.value);
  };
 
  return (
    <div>
      <input type="text" value={title} onChange={handleChange} />
 
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
 
      {toggle && <div>{title}</div>}
    </div>
  );
};

但是,在这种情况下,您可以完全省略 useEffect 的第二个参数 - 依赖数组 - 因为只有这两个变量会触发此组件的更新,因此如果没有第二个参数,因为只依赖一个props和state,副作用都会重新渲染。

让 React 的 useEffect 在更新的变量上运行有多种用例。 例如,在更新状态后,人们可能希望有一个基于此状态变化的回调函数。

REACT USEEFFECT HOOK: ONLY ON UPDATE

如果您已经仔细阅读了上一节,您就会知道 React 的 useEffect Hook 和一组依赖项也会在组件的第一次渲染时运行。 如果您只想在更新中运行此效果怎么办? 我们可以通过对实例变量使用 React 的 useRef Hook 来实现这一点:

const Toggler = ({ toggle, onToggle }) => {
  const didMount = React.useRef(false);
 
  React.useEffect(() => {
    if (didMount.current) {
      console.log('I run only if toggle changes.');
    } else {
      didMount.current = true;
    }
  }, [toggle]);
 
  return (
    <div>
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
 
      {toggle && <div>Hello React</div>}
    </div>
  );
};

当副作用函数第一次在挂载时运行时,它只会翻转实例变量,而不会运行副作用的实现细节(这里是 console.log)。 只有在下一次副作用运行时(在第一次重新渲染/更新组件时),真正的实现逻辑才会运行。

REACT USEEFFECT HOOK: ONLY ONCE

如您所见,通过传递一个空的依赖数组,您可以只运行一次 React 的 useEffect Hook 函数。 这只会在组件的第一次渲染时运行该函数一次。 如果您想为不同的情况运行 effect 函数 - 例如,当变量更新时只运行一次怎么办? 让我们来看看:

const Toggler = ({ toggle, onToggle }) => {
  const calledOnce = React.useRef(false);
 
  React.useEffect(() => {
    if (calledOnce.current) {
      return;
    }
 
    if (toggle === false) {
      console.log('I run only once if toggle is false.');
 
      calledOnce.current = true;
    }
  }, [toggle]);
 
  return (
    <div>
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
 
      {toggle && <div>Hello React</div>}
    </div>
  );
};

和以前一样,我们使用 React 的 useRef Hook 中的实例变量来实现它来跟踪非状态信息。 一旦满足我们的条件,例如这里布尔标志设置为 false,我们记得我们已经调用了效果的函数并且不再调用它。

REACT USEEFFECT HOOK: CLEANUP

有时您需要在组件重新渲染时从 React 的 useEffect Hook 清除您的效果。 幸运的是,这是 useEffect 的一个内置特性,它在 useEffects 的 effect 函数中返回了一个清理函数。 以下示例向您展示了使用 React 的 useEffect Hook 的计时器实现:

import * as React from 'react';
 
const App = () => {
  const [timer, setTimer] = React.useState(0);
 
  React.useEffect(() => {
    const interval = setInterval(() => setTimer(timer + 1), 1000);
 
    return () => clearInterval(interval);
  }, [timer]);
 
  return <div>{timer}</div>;
};
 
export default App;

当组件第一次渲染时,它会与 React 的 useEffect Hook 设置一个间隔,每 1 秒滴答一次。 一旦间隔滴答作响,计时器的状态就会增加 1。 状态更改启动组件的重新渲染。 由于计时器状态已更改,如果没有清除函数,useEffect 函数将再次运行并设置另一个间隔。 这不是我们想要的行为,因为毕竟我们只需要一个间隔。 这就是 useEffect 函数在组件更新之前清除间隔,然后组件设置新间隔的原因。 本质上,在此示例中,间隔仅运行一秒钟,然后才会被清除。

REACT USEEFFECT HOOK: UNMOUNT

useEffect 钩子的清理函数也在卸载组件时运行。 这对于应该在组件不再存在后停止运行的间隔或任何其他消耗内存的对象是有意义的。 在下面的 useEffect 示例中,我们将前面的示例替换为另一个版本:

import * as React from 'react';
 
const App = () => {
  const [timer, setTimer] = React.useState(0);
 
  React.useEffect(() => {
    const interval = setInterval(
      () => setTimer((currentTimer) => currentTimer + 1),
      1000
    );
 
    return () => clearInterval(interval);
  }, []);
 
  return <div>{timer}</div>;
};
 
export default App;

现在我们正在使用 useState 钩子的能力来使用函数而不是值来更新状态。 该函数将当前定时器作为参数。 因此,我们不再需要从外部提供计时器,并且只能在挂载(空依赖数组)上运行一次效果。 这就是为什么这里的清理函数只在组件卸载时调用(由于页面转换或条件渲染)。