纯函数只执行计算,不做其他事情。它让你的代码更容易理解和调试,并且允许 React 自动正确优化你的组件和 Hooks。
为什么纯粹很重要?
🌐 Why does purity matter?
使 React 成为 React 的关键概念之一是纯粹性。一个纯组件或钩子是指这样的组件或钩子:
🌐 One of the key concepts that makes React, React is purity. A pure component or hook is one that is:
- 幂等 – 当你使用相同的输入执行它时,你每次都会得到相同的结果 – 组件的输入包括 props、state 和 context;钩子输入包括参数。
- 在渲染中没有副作用 – 有副作用的代码应该与渲染分开运行。例如,作为事件处理程序——用户与界面交互并导致其更新;或者作为Effect——在渲染后运行。
- 不修改非本地值:组件和钩子在渲染时应绝不修改非本地创建的值。
当 render 保持纯粹时,React 可以理解如何优先处理哪些更新最重要,用户应首先看到。这之所以成为可能,是因为 render 的纯粹性:由于组件在 render 中没有副作用,React 可以暂停渲染那些不那么重要的组件,并且只在需要时再回过头来处理它们。
🌐 When render is kept pure, React can understand how to prioritize which updates are most important for the user to see first. This is made possible because of render purity: since components don’t have side effects in render, React can pause rendering components that aren’t as important to update, and only come back to them later when it’s needed.
具体来说,这意味着渲染逻辑可以多次运行,从而使 React 能够为你的用户提供愉快的用户体验。然而,如果你的组件有未跟踪的副作用——例如在渲染期间修改全局变量的值——当 React 再次运行你的渲染代码时,你的副作用将以与你期望不一致的方式被触发。这通常会导致意想不到的错误,从而降低用户使用应用的体验。你可以在《保持组件纯粹》页面中看到一个示例 。
🌐 Concretely, this means that rendering logic can be run multiple times in a way that allows React to give your user a pleasant user experience. However, if your component has an untracked side effect – like modifying the value of a global variable during render – when React runs your rendering code again, your side effects will be triggered in a way that won’t match what you want. This often leads to unexpected bugs that can degrade how your users experience your app. You can see an example of this in the Keeping Components Pure page.
React 如何运行你的代码?
🌐 How does React run your code?
React 是声明式的:你告诉 React 渲染 什么,React 会自己决定如何最好地将其显示给用户。为此,React 有几个阶段来运行你的代码。你不需要了解所有这些阶段也能很好地使用 React。但在高层次上,你应该知道哪些代码在 render 中运行,哪些运行在它之外。
🌐 React is declarative: you tell React what to render, and React will figure out how best to display it to your user. To do this, React has a few phases where it runs your code. You don’t need to know about all of these phases to use React well. But at a high level, you should know about what code runs in render, and what runs outside of it.
渲染 是指计算你的 UI 下一版本应该是什么样子。在渲染之后,React 会将这个新的计算结果与用于创建 UI 先前版本的计算结果进行比较。然后,React 只会将最小的必要更改提交到 DOM(用户实际上看到的内容)以应用这些更改。最后, Effects 会被刷新(意味着它们会一直运行直到没有剩余)。有关更详细的信息,请参阅 Render 和 Commit and Effect Hooks 的文档。
🌐 Rendering refers to calculating what the next version of your UI should look like. After rendering, React takes this new calculation and compares it to the calculation used to create the previous version of your UI. Then React commits just the minimum changes needed to the DOM (what your user actually sees) to apply the changes. Finally, Effects are flushed (meaning they are run until there are no more left). For more detailed information see the docs for Render and Commit and Effect Hooks.
深入研究
🌐 How to tell if code runs in render
判断代码是否在渲染期间运行的一个快速启发式方法是检查它的位置:如果它像下面的示例一样写在顶层,那么它很可能在渲染期间运行。
🌐 One quick heuristic to tell if code runs during render is to examine where it is: if it’s written at the top level like in the example below, there’s a good chance it runs during render.
function Dropdown() {
const selectedItems = new Set(); // created during render
// ...
}事件处理程序和效果不在渲染中运行:
🌐 Event handlers and Effects don’t run in 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]);
}组件和钩子必须是幂等的
🌐 Components and Hooks must be idempotent
组件必须始终根据其输入——props、state 和 context——返回相同的输出。这被称为_幂等性_。幂等性是函数式编程中流行的一个术语。它指的是这样一个概念:你每次运行相同的代码并使用相同的输入时都会得到相同的结果。
🌐 Components must always return the same output with respect to their inputs – props, state, and context. This is known as idempotency. Idempotency is a term popularized in functional programming. It refers to the idea that you always get the same result every time you run that piece of code with the same inputs.
这意味着在渲染期间运行的_所有_代码也必须是幂等的,以使此规则成立。例如,这行代码不是幂等的(因此,组件也不是):
🌐 This means that all code that runs during render must also be idempotent in order for this rule to hold. For example, this line of code is not idempotent (and therefore, neither is the component):
function Clock() {
const time = new Date(); // 🔴 Bad: always returns a different result!
return <span>{time.toLocaleString()}</span>
}new Date() 不是幂等的,因为它总是返回当前日期,每次调用时结果都会改变。当你渲染上述组件时,屏幕上显示的时间将保持在组件渲染时的时间。同样,像 Math.random() 这样的函数也不是幂等的,因为即使输入相同,每次调用它们时也会返回不同的结果。
这并不意味着你完全不应该使用非幂等函数,比如 new Date()——你只是在渲染时应避免使用它们。在这种情况下,我们可以使用 Effect 将最新日期同步到此组件:
🌐 This doesn’t mean you shouldn’t use non-idempotent functions like new Date() at all – you should just avoid using them during render. In this case, we can synchronize the latest date to this component using an Effect:
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() 调用封装在 Effect 中,它将该计算移到渲染之外。
🌐 By wrapping the non-idempotent new Date() call in an Effect, it moves that calculation outside of rendering.
如果你不需要将某些外部状态与 React 同步,如果它只需要在响应用户交互时更新,你也可以考虑使用 事件处理程序。
🌐 If you don’t need to synchronize some external state with React, you can also consider using an event handler if it only needs to be updated in response to a user interaction.
副作用必须在渲染之外运行
🌐 Side effects must run outside of render
副作用 不应在 渲染中 运行,因为 React 可能多次渲染组件以创建最佳的用户体验。
虽然渲染必须保持纯粹,但在某些时候副作用是必要的,以便你的应用可以做一些有趣的事情,比如在屏幕上显示内容!这条规则的关键点是副作用不应该在渲染中运行,因为 React 可能会多次渲染组件。在大多数情况下,你会使用事件处理器来处理副作用。使用事件处理器明确告诉 React 这段代码不需要在渲染期间运行,从而保持渲染的纯粹。如果你已用尽所有选项——并且仅作为最后的手段——你也可以使用 useEffect 来处理副作用。
🌐 While render must be kept pure, side effects are necessary at some point in order for your app to do anything interesting, like showing something on the screen! The key point of this rule is that side effects should not run in render, as React can render components multiple times. In most cases, you’ll use event handlers to handle side effects. Using an event handler explicitly tells React that this code doesn’t need to run during render, keeping render pure. If you’ve exhausted all options – and only as a last resort – you can also handle side effects using useEffect.
什么时候突变合适?
🌐 When is it okay to have mutation?
局部突变
🌐 Local mutation
副作用的一个常见例子是修改,在 JavaScript 中是指更改非原始值的值。总体来说,虽然在 React 中修改并不符合习惯用法,但_局部_修改是完全可以的:
🌐 One common example of a side effect is mutation, which in JavaScript refers to changing the value of a non-primitive value. In general, while mutation is not idiomatic in React, local mutation is absolutely fine:
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 来简洁写法,但创建一个局部数组然后在渲染过程中向其中推入项目是完全没问题的 during render。
🌐 There is no need to contort your code to avoid local mutation. Array.map could also be used here for brevity, but there is nothing wrong with creating a local array and then pushing items into it during render.
即使看起来我们在修改 items,关键点是该代码只在 局部 进行修改 —— 当组件再次渲染时,这种修改不会被“记住”。换句话说,items 只会在组件存在期间保持。因为 items 每次 <FriendList /> 渲染时都会被 重新创建,所以组件始终会返回相同的结果。
🌐 Even though it looks like we are mutating items, the key point to note is that this code only does so locally – the mutation isn’t “remembered” when the component is rendered again. In other words, items only stays around as long as the component does. Because items is always recreated every time <FriendList /> is rendered, the component will always return the same result.
另一方面,如果 items 是在组件之外创建的,它会保留之前的值并记住变化:
🌐 On the other hand, if items was created outside of the component, it holds on to its previous values and remembers changes:
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 /> 在 渲染过程中 有可观察的副作用,并且违反了规则。
🌐 When <FriendList /> runs again, we will continue appending friends to items every time that component is run, leading to multiple duplicated results. This version of <FriendList /> has observable side effects during render and breaks the rule.
延迟初始化
🌐 Lazy initialization
尽管不是完全“纯粹”的,延迟初始化也是可以的:
🌐 Lazy initialization is also fine despite not being fully “pure”:
function ExpenseForm() {
SuperCalculator.initializeIfNotReady(); // ✅ Good: if it doesn't affect other components
// Continue rendering...
}改变 DOM
🌐 Changing the DOM
在 React 组件的渲染逻辑中,不允许出现用户可以直接看到的副作用。换句话说,仅仅调用一个组件函数本身不应该在屏幕上产生变化。
🌐 Side effects that are directly visible to the user are not allowed in the render logic of React components. In other words, merely calling a component function shouldn’t by itself produce a change on the screen.
function ProductDetailPage({ product }) {
document.title = product.title; // 🔴 Bad: Changes the DOM
}实现希望在渲染之外更新 document.title 的一种方法是 将组件与 document 同步。
🌐 One way to achieve the desired result of updating document.title outside of render is to synchronize the component with document.
只要多次调用一个组件是安全的,并且不会影响其他组件的渲染,React 并不在意它在严格的函数式编程意义上是否是100%纯的。更重要的是组件必须是幂等的。
🌐 As long as calling a component multiple times is safe and doesn’t affect the rendering of other components, React doesn’t care if it’s 100% pure in the strict functional programming sense of the word. It is more important that components must be idempotent.
属性和状态是不可变的
🌐 Props and state are immutable
组件的 props 和 state 是不可变的 快照。不要直接修改它们。相反,应传递新的 props,并使用 useState 提供的 setter 函数。
🌐 A component’s props and state are immutable snapshots. Never mutate them directly. Instead, pass new props down, and use the setter function from useState.
你可以将 props 和 state 值看作在渲染后更新的快照。因此,你不会直接修改 props 或 state 变量:相反,你传递新的 props,或者使用提供给你的 setter 函数来告诉 React,下次组件渲染时需要更新 state。
🌐 You can think of the props and state values as snapshots that are updated after rendering. For this reason, you don’t modify the props or state variables directly: instead you pass new props, or use the setter function provided to you to tell React that state needs to update the next time the component is rendered.
不要改变属性
🌐 Don’t mutate Props
Props 是不可变的,因为如果你改变它们,应用将产生不一致的输出,这可能很难调试,因为它可能会或可能不会工作,具体取决于具体情况。
🌐 Props are immutable because if you mutate them, the application will produce inconsistent output, which can be hard to debug as it may or may not work depending on the circumstances.
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>;
}不要改变状态
🌐 Don’t mutate State
useState 返回状态变量以及用于更新该状态的设置函数。
const [stateVariable, setter] = useState(0);与其直接在原地更新状态变量,我们需要使用 useState 返回的 setter 函数来更新它。直接更改状态变量的值不会导致组件更新,这会让用户看到过时的 UI。使用 setter 函数可以告知 React 状态已经改变,并且我们需要排队重新渲染以更新 UI。
🌐 Rather than updating the state variable in-place, we need to update it using the setter function that is returned by useState. Changing values on the state variable doesn’t cause the component to update, leaving your users with an outdated UI. Using the setter function informs React that the state has changed, and that we need to queue a re-render to update the 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>
);
}钩子的返回值和参数是不可变的
🌐 Return values and arguments to Hooks are immutable
一旦值传递给 hook,就不应修改它们。就像 JSX 中的 props 一样,值在传递给 hook 时会变为不可变。
🌐 Once values are passed to a hook, you should not modify them. Like props in JSX, values become immutable when passed to a 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 可能使用它的参数作为依赖,以便在内部进行值的记忆化:
🌐 One important principle in React is local reasoning: the ability to understand what a component or hook does by looking at its code in isolation. Hooks should be treated like “black boxes” when they are called. For example, a custom hook might have used its arguments as dependencies to memoize values inside it:
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
return useMemo(() => {
const newIcon = { ...icon };
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}, [icon, theme]);
}如果你要修改钩子的参数,自定义钩子的记忆将变得不正确,因此务必避免这样做。
🌐 If you were to mutate the Hook’s arguments, the custom hook’s memoization will become incorrect, so it’s important to avoid doing that.
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 returnedstyle = 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同样,重要的是不要修改钩子的返回值,因为它们可能已被记忆。
🌐 Similarly, it’s important to not modify the return values of Hooks, as they may have been memoized.
值在传递给 JSX 后是不可变的
🌐 Values are immutable after being passed to JSX
不要在 JSX 中使用值之后再修改它们。将修改操作移动到创建 JSX 之前。
🌐 Don’t mutate values after they’ve been used in JSX. Move the mutation to before the JSX is created.
当你在表达式中使用 JSX 时,React 可能会在组件完成渲染之前就积极地评估 JSX。这意味着在将值传递给 JSX 之后进行修改可能会导致 UI 过时,因为 React 不会知道更新组件的输出。
🌐 When you use JSX in an expression, React may eagerly evaluate the JSX before the component finishes rendering. This means that mutating values after they’ve been passed to JSX can lead to outdated UIs, as React won’t know to update the component’s output.
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}
</>
);
}