首页 / 文章 / React片段与StrictMode:更简洁的标记结构及更早的错误检测

React片段与StrictMode:更简洁的标记结构及更早的错误检测

了解 React Fragment 如何在无需额外 DOM 包装的情况下对元素进行分组,以及 StrictMode 的仅开发环境使用的双重调用机制如何揭示不纯净的渲染操作和副作用。

976 词

有两个 React 特性虽然从不直接在屏幕上绘制任何内容,但却影响着几乎所有的组件结构:片段(Fragments)和 StrictMode。片段能够避免 DOM 中出现仅为满足 JSX 要求而存在的包装元素,而 StrictMode 则会在开发阶段刻意对组件进行压力测试,以便在用户发现之前就暴露出有问题的逻辑。

为什么 JSX 需要单一的根节点

JSX 会将每个标签编译为一次函数调用,因此当两个同级标签一起被返回时,实际上会得到两个值,而 JSX 期望的只是一个值。这样会导致编译失败:

return (
  <h1>Hello</h1>
  <p>Welcome</p>
);

传统的解决办法是将这两个同级标签包裹在 div 中:

return (
  <div>
    <h1>Hello</h1>
    <p>Welcome</p>
  </div>
);

这种额外的节点会带来诸多问题:它会使 DOM 结构更加复杂,破坏那些期望直接子元素的 flex 或 grid 布局,增加选择器的复杂性,还可能生成无效或可访问性较差的 HTML。

无需包装元素的组合方式

React中的Fragment用于组合子元素而不生成任何DOM元素,其简洁的语法表现为一对空标签:

return (
  <>
    <h1>Hello</h1>
    <p>Welcome</p>
  </>
);

显式形式React.Fragment功能相同,但在需要传递属性时必须使用这种形式:

return (
  <React.Fragment>
    <h1>Hello</h1>
    <p>Welcome</p>
  </React.Fragment>
);

渲染后的HTML仅包含

和

元素。虽然性能提升微乎其微,但其真正的好处在于能确保标记的正确性。

返回同级元素

若一个组件生成多个同级元素,可将其布局交给父组件处理:

function Card() {
  return (
    <>
      <h2>Title</h2>
      <p>Description</p>
    </>
  );
}

列表中的带键Fragment

当遍历的数据中每个项对应多个元素时,React仍需要为每个项提供一个稳定的key。简洁的<>语法不支持属性,因此应使用带有键的React.Fragment:

items.map(item => (
  <React.Fragment key={item.id}>
    <h2>{item.title}</h2>
    <p>{item.description}</p>
  </React.Fragment>
));

表格单元格与行

HTML 表格有严格的内容模型:一个 tr 元素只能包含 td 或 th 元素,不能使用 div 作为容器。而 Fragment 则允许某个组件为其父元素所在的行贡献多个单元格:

function Row() {
  return (
    <>
      <td>A</td>
      <td>B</td>
    </>
  );
}

StrictMode 检查的内容

StrictMode 在生产环境中不会渲染任何内容、不添加任何元素,也没有实际作用。在开发模式下,它会进行额外的检查,包括:

  • 对那些被认为不安全的旧版类生命周期方法发出警告,例如 componentWillMount
  • 对已废弃的 API 发出警告,比如字符串引用和 findDOMNode
  • 重复调用组件的主体、初始化函数和更新函数,以识别不纯净的渲染行为
  • 在组件挂载时通过额外的设置、清理和重新设置流程来检测是否存在未完成的清理操作

不同版本的 React 中,具体列表会有所变化,因此请查看对应版本的最新文档。

刻意的双次渲染

设想这样一个在渲染时会输出日志的组件:

function App() {
  console.log("Rendered!");
  return <h1>Hello</h1>;
}

在开发模式下的 StrictMode 下,控制台会显示两次该消息:

Rendered!
Rendered!

这是有意为之。渲染函数应当是纯函数:在相同的属性和状态输入下,它应始终返回相同的结果,并且不会修改自身之外的任何内容。如果调用该函数两次,就会导致诸如修改共享变量之类的违规行为,从而产生明显错误的结果。函数的纯度非常重要,因为并发渲染可能会使渲染开始、暂停、丢弃或重复执行,而那些假设每次更新只进行一次渲染的代码在这种情况下就会出问题。

启用该模式

将需要检查的组件树结构,通常是整个应用,包裹在根节点中:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

StrictMode下的影响:依赖数组并非解决方案

这是一个没有依赖数组的效应。它会在每次渲染后执行,因此每次更新都会触发另一次请求:

useEffect(() => {
  console.log("Fetching...");
  fetch("/api");
});

添加空数组可以将效应的执行限制在组件挂载时:

useEffect(() => {
  console.log("Fetching...");
  fetch("/api");
}, []); // stable dependency

这样的修改是正确的,但无法避免开发环境中的重复请求。从React 18开始,StrictMode会先挂载组件、执行其清理函数,然后再重新挂载,因此使用[]的效应仍会执行两次。重复执行是现象而非错误,真正的解决方案是编写一个清理函数,让第二次执行不会造成问题,通常可以通过AbortController中止第一次请求,或用标志位忽略其结果。这样的效应在正式环境中也能安全应对真正的重新挂载。

对比展示

  • 目的:Fragment用于对元素进行分组;StrictMode则用于发现错误。
  • DOM输出:两者都不会添加新元素。
  • 实际应用影响:两者均无影响。
  • 运行时行为:Fragment会正常渲染;而StrictMode仅在开发环境下会对渲染和操作进行双重调用。
  • 优势:可以获得更简洁、合法的标记结构,同时获得可预测且无副作用的代码。

可供尝试的最简配置

下面的组件通过Fragment返回两个兄弟元素:

export default function App() {
  return (
    <>
      <h1>Hello World</h1>
      <p>Rendered using Fragments</p>
    </>
  );
}

入口文件会在StrictMode环境下渲染它,因此之后添加的任何非纯逻辑都会在开发阶段立即被检测出来:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

关键要点

  • 每当只需要一个包装元素来满足JSX要求时,就使用Fragment,并在列表中配合React.Fragment和key使用。
  • 在开发过程中请保持StrictMode处于启用状态;重复的日志和效果是刻意设计的诊断手段。
  • 应通过 proper 的清理机制来解决效果重复执行的问题,而非随意调整依赖项。