首页 / 文章 / 实用提示:React 组件必须是纯函数——StrictMode 背后的秘密

实用提示:React 组件必须是纯函数——StrictMode 背后的秘密

《实用笔记》操作指南:React 组件必须保持纯函数特性——StrictMode 背后的秘密:适用于采用该模式的团队的契约、检查机制以及可直接插入的代码片段。

1954 词

可将此内容视为《React组件必须保持纯函数状态——StrictMode与编译器背后的秘密》一文中理念的面向操作员的重构版本:清晰的阶段划分、有序的代码结构,以及便于交接时参考的恢复说明。在扩大范围之前,最好将“概览”阶段视为一个可量化的基准面,记录一份最佳实现示例、一个故障案例以及回滚说明。应将此阶段视为输入与经过验证的输出之间的契约,为相关成果命名、定义成功标准,并杜绝默许的半完成状态。

什么是纯函数?

在“什么是纯函数”阶段,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。应在功能结果旁记录执行时间以及令牌或查询成本。提前显示成本信息,可避免在从演示环境切换到共享环境时出现意外费用。应将状态与负责数据变更的组件放在一起处理;若将所有数据都存放在全局存储中,就会使时间相关的问题更难被发现。

// ✅ pure function — same input, same output, and it touches nothing outside
function double(x: number): number {
  return x * 2;
}
double(3); // 6
double(3); // 6 — six no matter how many times you call it

// 🔴 impure function — it changes the outer variable `total` (a side effect)
let total = 0;
function addToTotal(x: number): number {
  total += x;     // side effect!
  return total;   // the result changes on every call
}
addToTotal(3); // 3
addToTotal(3); // 6 — same input, different result

组件也是纯函数

对于属于某个阶段的A组件,在修改代码之前需明确其输入参数、该步骤的负责人以及结束条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,这样操作人员无需查看整个系统结构即可进行审计。 状态信息应与负责数据变更的组件放在一起。将所有内容都存放在全局存储中会使得时序错误更难被发现。

type Props = { name: string };

// ✅ pure — same name, always the same result
function Greeting({ name }: Props) {
  return <h1>Hello, {name}!</h1>;
}

违反规则——渲染期间切勿修改外部内容

在“打破规则”阶段,修改代码之前需明确输入参数、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和异常恢复路径。重试机制、人工审核环节以及错误处理都是产品本身的组成部分,而非后续需要补充的功能。 状态应与负责数据变更的组件放在一起管理。将所有数据都存放在全局存储中会使得时序错误更难被发现。 在“打破规则”阶段,修改代码之前需明确输入参数、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 应将此阶段视为输入与验证后输出之间的契约。为相关工件命名,明确成功判定标准,杜绝无声的半完成状态。

let count = 0; // a variable outside the component

// 🔴 not pure — changes an external variable while rendering
function Counter() {
  count = count + 1; // side effect!
  return <p>{count}</p>;
}

自己动手实现——为何会出现“2”?

在“自己动手实现”阶段,首先需列出相关约定:所需输入、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免从演示环境过渡到共享环境时出现意外费用。 应将各种效果视为与外部世界的同步机制,而非渲染过程中衍生值的替代品。

import { useState } from 'react';

let count = 0;

function Counter() {
  const [, setTick] = useState(0); // a device to trigger re-renders
  count = count + 1; // 🔴 changes an external variable on every render
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setTick((n) => n + 1)}>Re-render</button>
    </div>
  );
}
// ✅ pure — computed from input (props) alone
function Counter({ count }: { count: number }) {
  return <p>{count}</p>;
}

但你可以更改“渲染过程中生成的内容”

在处理“但你可以改变”阶段时,首先写下相关契约:所需的输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 将配置信息与应用程序代码分开存放。环境文件、密钥存储以及功能开关应集中于一个位置,这样操作人员无需查看整个系统结构即可进行审计。 应将各种效应视为与外部世界的同步机制,而非渲染过程中衍生值的替代品。

function ProductList({ products }: { products: Product[] }) {
  // ✅ this array was just created in this render — handle it however you like
  const sorted = [...products].sort((a, b) => a.price - b.price);
  return (
    <ul>
      {sorted.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

谜题一揭晓——为何StrictMode会让代码执行两次

在完成“Mystery 1 Solved Why”阶段时,首先需写下契约:所需的输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 同时记录正常流程和恢复流程。重试、人工审核以及死信处理都是产品的一部分,而非后续的优化工作。 应将各种效应视为与外部世界的同步机制,而非渲染过程中派生值的替代品。 在完成“Mystery 1 Solved Why”阶段时,首先需写下契约:所需的输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 将此阶段视为输入与已验证输出之间的契约。为相关产物命名,定义成功检查标准,并拒绝默许的部分完成状态。

// main.tsx — in development, the components inside this render twice
createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

那么副作用该去哪儿呢?

将 Side Stage 视为可度量的界面时,它的作用最为显著。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。在功能结果旁同时记录执行时间以及令牌或查询成本。提前了解成本情况,就能避免在从演示环境过渡到共享环境时出现意外账单。尽量保持渲染工作的成本较低,只有在经过测量后才将高成本的推导操作放在记忆化之后处理。过早进行记忆化可能会掩盖过时的属性错误。

// 🔴 side effect during rendering — fires on every render
function ProductPage({ id }: { id: number }) {
  logView(id); // a side effect in the render body — not allowed
  return <h1>Product {id}</h1>;
}

// ✅ in an event handler — runs only at the moment the user clicks
function BuyButton({ id }: { id: number }) {
  return <button onClick={() => logPurchase(id)}>Buy</button>;
}

谜题二已解——为何 React 编译器依赖纯度

《谜题2》已解:为何将界面视为可测量的表面时效果最佳。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。 将配置置于应用程序代码之外。环境文件、密钥存储和功能标志应集中存放,以便操作人员无需查看整个结构即可进行审计。 保持渲染操作的成本较低,只有在经过测量后才会将耗时的计算放在记忆化机制之后处理。过早的记忆化可能会掩盖过时属性带来的错误。

// 🔴 not pure → the compiler skips optimization (ESLint points at this component)
let renderCount = 0;
function RenderCounter() {
  renderCount++; // mutating an external variable during render — a violation
  return <p>{renderCount}</p>;
}

// ✅ pure → the compiler memoizes automatically (reuse the previous result when inputs match)
function Label({ text }: { text: string }) {
  return <p>{text}</p>;
}

function App() {
  return (
    <>
      <RenderCounter />
      <Label text="Pure Component" />
    </>
  );
}

export default App;

将UI视为树结构

将 Seeing UI 视为舞台时,若能将其当作可度量的界面来处理,效果最佳。在扩大范围之前,先记录一份理想状态下的操作日志、一个故障案例以及回滚说明。 同时记录正常流程与恢复流程。重试机制、人工审核环节以及死信处理都是产品本身的一部分,而非后续需要补充的功能。 尽量降低渲染成本,只有在经过测量后才考虑通过记忆化手段来处理高成本的计算任务。过早使用记忆化可能会掩盖过时的属性相关错误。 将 Seeing UI 视为舞台时,若能将其当作可度量的界面来处理,效果最佳。在扩大范围之前,先记录一份理想状态下的操作日志、一个故障案例以及回滚说明。 应将这个阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,绝不允许出现无声无息的半完成状态。

function App() {
  return <ProductList products={products} />;
}

function ProductList({ products }: { products: Product[] }) {
  return (
    <ul>
      {products.map((p) => (
        <ProductCard key={p.id} product={p} />
      ))}
    </ul>
  );
}
App
└─ ProductList
   ├─ ProductCard
   ├─ ProductCard
   └─ ProductCard

总结

在收尾阶段,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。除了功能结果外,还需记录执行时间以及令牌或查询成本。提前了解这些成本可以避免在从演示环境过渡到共享环境时出现意外费用。状态应与负责数据变更的组件放在一起管理,将所有数据都存放在全局存储中会使得时间相关的问题更难被发现。

参考资料

在“参考阶段”,在修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,这样操作人员无需查看整个系统结构即可进行审计。 状态信息应与负责执行变更的组件放在一起。将所有数据都存放在全局存储中会使得时序错误更难被发现。

操作检查清单

在完成“操作检查清单阶段”时,首先需明确相关约定:所需的输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改符合预期。

应优先选择小型、可测试的单元,而非庞大的脚本。当某个步骤出错时,错误应指向单一责任点,而非复杂的流程链。

应将效果视为与外部世界的同步机制,而非渲染过程中替代衍生值的手段。

锁定依赖版本,并记录用于运行演示的图像摘要。可重复性比团队内部经验更为重要。

将此阶段视为输入与经过验证的输出之间的契约。为输出结果命名,明确成功标准,拒绝默许的不完整完成状态。

应将效果视为与外部世界的同步机制,而非渲染过程中替代衍生值的手段。

在推广该技术栈之前,应先冻结版本,为关键流程记录标准输出日志,并明确回滚步骤。共享环境需要设置速率限制、租户验证机制,以及负责密钥轮换的明确责任人。与其展示花哨的一次性演示,不如注重扎实的可靠性。

d707b555add8的批处理说明:不要将提供商密钥放入代码仓库,为每个会话设置令牌使用上限,并将日志存储在评估用示例文件旁边,以便后续模型更换时仍能保持对比性。