组件和 Hook 必须是纯的

纯函数只执行计算,不做其他任何事情。这使得你的代码更易于理解、调试,并允许 React 自动正确地优化你的组件和 Hook。

注意

此参考页面涵盖高级主题,需要熟悉保持组件纯净页面中涵盖的概念。

为什么纯度很重要?

使 React 成为 React 的关键概念之一就是 纯度。纯组件或 Hook 是指:

当保持渲染纯净时,React 可以理解如何确定哪些更新对用户来说最重要,并优先显示它们。这是因为渲染的纯度:由于组件在渲染中没有副作用,React 可以暂停渲染不那么重要的组件,只在需要时才返回更新它们。

具体来说,这意味着渲染逻辑可以多次运行,从而使 React 能够为用户提供良好的用户体验。但是,如果你的组件具有未跟踪的副作用(例如在渲染期间修改全局变量的值),则当 React 再次运行你的渲染代码时,你的副作用将会以一种与你期望不符的方式被触发。这通常会导致意外的错误,从而降低用户体验你的应用程序的方式。你可以在“保持组件纯净”页面中看到一个示例。

React 如何运行你的代码?

React 是声明式的:你告诉 React 要渲染*什么*,React 会找出*如何*最好地将其显示给用户。为此,React 有几个阶段来运行你的代码。你不需要了解所有这些阶段就可以很好地使用 React。但从高层次上讲,你应该了解*渲染*中运行的代码以及渲染之外运行的代码。

渲染是指计算 UI 的下一个版本应该是什么样子。渲染完成后,副作用 会被 刷新(意味着它们会一直运行,直到没有剩余的副作用),并且如果副作用对布局有影响,则可能会更新计算结果。React 会获取这个新的计算结果,并将其与用于创建 UI 上一个版本的计算结果进行比较,然后 提交DOM(用户实际看到的内容)的最小必要更改,以使其与最新版本保持一致。

深入探讨

如何判断代码是否在渲染时运行

判断代码是否在渲染时运行的一个快速方法是检查它所在的位置:如果它像下面的示例一样写在顶层,那么它很可能是在渲染时运行的。

function Dropdown() {
const selectedItems = new Set(); // created during render
// ...
}

事件处理程序和副作用不会在渲染时运行

function Dropdown() {
const selectedItems = new Set();
const onSelect = (item) => {
// this code is in an event handler, so it's only run when the user triggers this
selectedItems.add(item);
}
}
function Dropdown() {
const selectedItems = new Set();
useEffect(() => {
// this code is inside of an Effect, so it only runs after rendering
logForAnalytics(selectedItems);
}, [selectedItems]);
}

组件和 Hooks 必须是幂等的

对于相同的输入(属性、状态和上下文),组件必须始终返回相同的输出。这被称为 幂等性幂等性 是函数式编程中流行的一个术语。它指的是每次使用相同的输入运行一段代码时,你 总是得到相同的结果

这意味着为了遵守此规则,在渲染期间 运行的 所有 代码也必须是幂等的。例如,这行代码不是幂等的(因此,组件也不是幂等的)

function Clock() {
const time = new Date(); // 🔴 Bad: always returns a different result!
return <span>{time.toLocaleString()}</span>
}

new Date() 不是幂等的,因为它总是返回当前日期,并且每次调用时都会更改其结果。当你渲染上面的组件时,屏幕上显示的时间将停留在组件渲染时的那个时间。类似地,像 Math.random() 这样的函数也不是幂等的,因为即使输入相同,它们每次调用时也会返回不同的结果。

这并不意味着你不应该 完全 使用像 new Date() 这样的非幂等函数——你应该避免 在渲染期间 使用它们。在这种情况下,我们可以使用 副作用 将最新日期 同步 到此组件

import { useState, useEffect } from 'react';

function useTime() {
  // 1. Keep track of the current date's state. `useState` receives an initializer function as its
  //    initial state. It only runs once when the hook is called, so only the current date at the
  //    time the hook is called is set first.
  const [time, setTime] = useState(() => new Date());

  useEffect(() => {
    // 2. Update the current date every second using `setInterval`.
    const id = setInterval(() => {
      setTime(new Date()); // ✅ Good: non-idempotent code no longer runs in render
    }, 1000);
    // 3. Return a cleanup function so we don't leak the `setInterval` timer.
    return () => clearInterval(id);
  }, []);

  return time;
}

export default function Clock() {
  const time = useTime();
  return <span>{time.toLocaleString()}</span>;
}

通过将非幂等的 new Date() 调用包装在一个副作用中,它将该计算移动到 渲染之外

如果你不需要将某些外部状态与 React 同步,你也可以考虑使用 事件处理程序,如果它只需要响应用户交互进行更新。


副作用必须在渲染之外运行

副作用 不应 在渲染时 运行,因为 React 可以多次渲染组件以创建最佳的用户体验。

注意

副作用是一个比 Effect 更广泛的术语。Effect 特指包装在 useEffect 中的代码,而副作用是指除了将值返回给调用者的主要结果之外,还会产生任何可观察到的效果的代码的通用术语。

副作用通常写在 事件处理程序 或 Effect 中。但绝不能在渲染时运行。

虽然渲染必须保持纯净,但副作用在某些时候是必要的,以便你的应用程序执行任何有趣的操作,比如在屏幕上显示内容!此规则的关键在于,副作用不应 在渲染时 运行,因为 React 可以多次渲染组件。在大多数情况下,你将使用 事件处理程序 来处理副作用。使用事件处理程序明确地告诉 React,这段代码不需要在渲染时运行,从而保持渲染的纯净。如果你已经用尽了所有选项,并且仅作为最后的手段,你也可以使用 useEffect 来处理副作用。

什么时候可以进行突变?

局部突变

副作用的一个常见例子是突变,在 JavaScript 中,突变是指更改非 原始值 的值。一般来说,虽然突变在 React 中并不常见,但 局部 突变是绝对可以的

function FriendList({ friends }) {
const items = []; // ✅ Good: locally created
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // ✅ Good: local mutation is okay
}
return <section>{items}</section>;
}

不需要为了避免局部突变而扭曲你的代码。Array.map 也可以在这里使用以简化代码,但在 渲染期间 创建一个本地数组,然后将元素推送到其中并没有什么问题。

即使看起来我们在修改 items,但需要注意的关键是,此代码仅在*局部*执行此操作 – 当组件再次渲染时,不会“记住”修改。换句话说,items 的存在时间只与组件一样长。因为每次渲染 <FriendList /> 时,都会*重新创建* items,所以组件将始终返回相同的结果。

另一方面,如果在组件外部创建 items,它会保留其先前的值并记住更改

const items = []; // 🔴 Bad: created outside of the component
function FriendList({ friends }) {
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // 🔴 Bad: mutates a value created outside of render
}
return <section>{items}</section>;
}

<FriendList /> 再次运行时,我们将在每次运行该组件时继续将 friends 追加到 items,从而导致多个重复的结果。此版本的 <FriendList /> 在渲染 期间 会产生明显的副作用,并违反了规则

懒惰初始化

懒惰初始化也是可以的,尽管它不是完全“纯”的

function ExpenseForm() {
SuperCalculator.initializeIfNotReady(); // ✅ Good: if it doesn't affect other components
// Continue rendering...
}

更改 DOM

在 React 组件的渲染逻辑中不允许出现用户可以直接看到的副作用。换句话说,仅仅调用一个组件函数本身不应该在屏幕上产生任何变化。

function ProductDetailPage({ product }) {
document.window.title = product.title; // 🔴 Bad: Changes the DOM
}

在渲染之外更新 window.title 的一种方法是 将组件与 window 同步

只要多次调用一个组件是安全的,并且不会影响其他组件的渲染,React 并不关心它是否在函数式编程的严格意义上是 100% 纯的。更重要的是,组件必须是幂等的


属性和状态是不可变的

组件的属性和状态是不可变的 快照。切勿直接修改它们。相反,请向下传递新的属性,并使用 useState 中的设置器函数。

您可以将属性和状态值视为在渲染后更新的快照。因此,您不要直接修改属性或状态变量:而是传递新的属性,或使用提供给您的设置器函数来告诉 React 在下次渲染组件时需要更新状态。

不要修改属性

属性是不可变的,因为如果您修改它们,应用程序将产生不一致的输出,这可能很难调试,因为它可能会或可能不会根据情况起作用。

function Post({ item }) {
item.url = new Url(item.url, base); // 🔴 Bad: never mutate props directly
return <Link url={item.url}>{item.title}</Link>;
}
function Post({ item }) {
const url = new Url(item.url, base); // ✅ Good: make a copy instead
return <Link url={url}>{item.title}</Link>;
}

不要修改状态

useState 返回状态变量和用于更新该状态的设置器。

const [stateVariable, setter] = useState(0);

我们需要使用 useState 返回的设置器函数来更新状态变量,而不是就地更新它。更改状态变量上的值不会导致组件更新,从而使您的用户使用过时的 UI。使用设置器函数会通知 React 状态已更改,并且我们需要排队重新渲染以更新 UI。

function Counter() {
const [count, setCount] = useState(0);

function handleClick() {
count = count + 1; // 🔴 Bad: never mutate state directly
}

return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
function Counter() {
const [count, setCount] = useState(0);

function handleClick() {
setCount(count + 1); // ✅ Good: use the setter function returned by useState
}

return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}

Hook 的返回值和参数是不可变的

将值传递给 Hook 后,您不应修改它们。与 JSX 中的属性一样,值在传递给 Hook 后会变为不可变的。

function useIconStyle(icon) {
const theme = useContext(ThemeContext);
if (icon.enabled) {
icon.className = computeStyle(icon, theme); // 🔴 Bad: never mutate hook arguments directly
}
return icon;
}
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
const newIcon = { ...icon }; // ✅ Good: make a copy instead
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}

React 中的一个重要原则是*局部推理*:通过单独查看组件或 Hook 的代码来理解其功能的能力。调用 Hook 时,应将其视为“黑盒”。例如,自定义 Hook 可能已将其参数用作依赖项,以在其内部记忆值

function useIconStyle(icon) {
const theme = useContext(ThemeContext);

return useMemo(() => {
const newIcon = { ...icon };
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}, [icon, theme]);
}

如果您要修改 Hook 参数,则自定义 Hook 的记忆将变得不正确,因此避免这样做非常重要。

style = useIconStyle(icon); // `style` is memoized based on `icon`
icon.enabled = false; // Bad: 🔴 never mutate hook arguments directly
style = useIconStyle(icon); // previously memoized result is returned
style = useIconStyle(icon); // `style` is memoized based on `icon`
icon = { ...icon, enabled: false }; // Good: ✅ make a copy instead
style = useIconStyle(icon); // new value of `style` is calculated

同样,不要修改 Hook 的返回值也很重要,因为它们可能已经被记忆了。


值在传递给 JSX 后是不可变的

不要在 JSX 中使用值后修改它们。在创建 JSX 之前移动修改。

当您在表达式中使用 JSX 时,React 可能会在组件完成渲染之前急切地计算 JSX。这意味着在将值传递给 JSX 后对其进行更改可能会导致 UI 过时,因为 React 不会知道要更新组件的输出。

function Page({ colour }) {
const styles = { colour, size: "large" };
const header = <Header styles={styles} />;
styles.size = "small"; // 🔴 Bad: styles was already used in the JSX above
const footer = <Footer styles={styles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}
function Page({ colour }) {
const headerStyles = { colour, size: "large" };
const header = <Header styles={headerStyles} />;
const footerStyles = { colour, size: "small" }; // ✅ Good: we created a new value
const footer = <Footer styles={footerStyles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}