纯函数仅执行计算,仅此而已。它使你的代码更易于理解、调试,并允许 React 自动正确优化你的组件和钩子。
¥Pure functions only perform a calculation and nothing more. It makes your code easier to understand, debug, and allows React to automatically optimize your components and Hooks correctly.
为什么纯粹很重要?
¥Why does purity matter?
React 的关键概念之一就是纯粹性。纯组件或钩子是这样的:
¥One of the key concepts that makes React, React is purity. A pure component or hook is one that is:
-
幂等 - 你 每次总是得到相同的结果 使用相同的输入运行它 - 组件输入的属性、状态、上下文;以及钩子输入的参数。
¥Idempotent – You always get the same result every time you run it with the same inputs – props, state, context for component inputs; and arguments for hook inputs.
-
渲染中没有副作用 - 有副作用的代码应该运行 与渲染分开。例如,作为 事件处理程序 – 用户与 UI 交互并导致其更新;或作为 副作用 – 在渲染后运行。
¥Has no side effects in render – Code with side effects should run separately from rendering. For example as an event handler – where the user interacts with the UI and causes it to update; or as an Effect – which runs after render.
-
不改变非本地值:组件和钩子应该在渲染时 永远不要修改不是本地创建的值。
¥Does not mutate non-local values: Components and Hooks should never modify values that aren’t created locally in render.
当渲染保持纯粹时,React 可以理解如何优先考虑哪些更新对于用户最先看到的最重要。这是由于渲染纯度而成为可能的:由于组件没有副作用 渲染中,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。但在较高层面上,你应该了解哪些代码在渲染中运行以及哪些代码在渲染之外运行。
¥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 的先前版本的计算进行比较,然后仅提交对 DOM(用户实际看到的)所需的最少更改,以使其赶上最新版本。
¥Rendering refers to calculating what the next version of your UI should look like. After rendering, Effects are flushed (meaning they are run until there are no more left) and may update the calculation if the Effects have impacts on layout. React takes this new calculation and compares it to the calculation used to create the previous version of your UI, then commits just the minimum changes needed to the DOM (what your user actually sees) to catch it up to the latest version.
深入研究
¥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
组件必须始终返回与其输入相同的输出 - 属性、状态和上下文。这称为幂等性。幂等性 是函数式编程中流行的一个术语。它指的是你使用相同的输入运行该代码段的想法。
¥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()
is not idempotent as it always returns the current date and changes its result every time it’s called. When you render the above component, the time displayed on the screen will stay stuck on the time that the component was rendered. Similarly, functions like Math.random()
also aren’t idempotent, because they return different results every time they’re called, even when the inputs are the same.
这并不意味着你根本不应该使用像 new Date()
这样的非幂等函数 - 你应该避免使用它们 渲染期间。在这种情况下,我们可以使用 副作用 将最新日期同步到该组件:
¥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()
调用封装在副作用中,它将计算移动到 渲染之外。
¥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 可以多次渲染组件以创造最佳的用户体验。
¥Side effects should not run in render, as React can render components multiple times to create the best possible user experience.
虽然渲染必须保持纯粹,但在某些时候,为了让你的应用执行任何有趣的操作(例如在屏幕上显示某些内容),副作用是必要的!该规则的关键点是副作用不应该运行 渲染中,因为 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 中指的是更改非 primitive 值的值。一般来说,虽然突变在 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
,但是创建本地数组然后将项目推入其中 渲染期间 并没有什么问题。
¥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
只会随着组件的存在而存在。因为每次渲染 <FriendList />
时总是重新创建 items
,所以组件将始终返回相同的结果。
¥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.window.title = product.title; // 🔴 Bad: Changes the DOM
}
在渲染之外实现更新 window.title
所需结果的一种方法是更新 将组件与 window
同步。
¥One way to achieve the desired result of updating window.title
outside of render is to synchronize the component with window
.
只要多次调用一个组件是安全的并且不会影响其他组件的渲染,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
组件的属性和状态是不可变的 快照。切勿直接突变它们。相反,向下传递新的属性,并使用 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
.
你可以将属性和状态值视为渲染后更新的快照。因此,你不要直接修改属性或状态变量:相反,你传递新的属性,或者使用提供给你的 setter 函数来告诉 React 状态需要在下次渲染组件时更新。
¥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 are immutable because if you mutate them, the application will produce inconsistent output, which can be hard to debug since it may or may not work depending on the circumstance.
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
返回状态变量和更新该状态的设置器。
¥useState
returns the state variable and a setter to update that state.
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
一旦值被传递给钩子,你就不应该修改它们。就像 JSX 中的属性一样,值在传递给钩子时变得不可变。
¥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 中的一项重要原则是局部推断:通过单独查看组件或钩子的代码来理解其功能的能力。调用钩子时应将其视为 “黑匣子”。例如,自定义钩子可能使用其参数作为依赖来记忆其中的值:
¥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 Hooks 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 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
同样,重要的是不要修改钩子的返回值,因为它们可能已被记忆。
¥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 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}
</>
);
}