React起步&前端汇总

前端工具

基础概念&定位

前端工具

概念对应工具它的职责(物流比喻)
包管理器 (Package Manager)npm, yarn, pnpm采购员:负责从网上(Registry)把别人写好的插件、库买回来,放在 node_modules 仓库里。
构建工具 (Build Tool)Vite, Webpack, Turbopack生产流水线:负责把你写的代码和买回来的插件进行加工、翻译(如把 React 翻译成 JS)、压缩,最后打包成浏览器能看懂的成品。
全栈框架 (Framework)Next.js, Nuxt.js总设计师:决定整个工厂怎么盖,流水线怎么排,甚至连后端服务器怎么跑都规划好了。

对比 react / vue

功能模块Vue 生态 (官方统筹)React 生态 (社区/官方推荐)备注
核心框架Vue (SFC, 响应式)React (JSX, Hooks)Vue 像自动挡,React 像手动挡。
路由管理Vue RouterReact Router / TanStack RouterReact Router 是事实上的标准。
状态管理Pinia (或旧的 Vuex)Zustand / Recoil / Context APIPinia 对应的社区顶流现在是 Zustand
构建工具ViteVite / Turbopack两者现在都首选 Vite。
全栈框架Nuxt.jsNext.js所谓的“全站/元框架”。

vue丝滑路线

vue2 -> vue router -> vuex -> 单页HTML引入vue -> vue2脚手架使用webpack构建vue项目,单文件组件 - > vue3+vite构建项目(pinia)(后续的nuxtjs我并没学)

React 的“断层式”发展路线

React 的历史更久,它的路线不是线性的,而是经历了三次“哲学大迁徙”

阶段一:纯 UI 库时代 —— 对应你的 Vue 2 阶段

关键词: Class Components, Lifecycle, Webpack.

路径: 引入 react.js $\rightarrow$ 写 class 组件 $\rightarrow$ 用 Redux 管理状态 $\rightarrow$ 用 Create React App (CRA) 构建。

感受: 逻辑和当时的 Vue 2 差不多,只是语法更底层。

阶段二:Hooks 革命 —— 对应你的 Vue 3 阶段

关键词: Function Components, Hooks (useState, useEffect).

路径: 抛弃 Class $\rightarrow$ 全面拥抱函数式编程 $\rightarrow$ Vite 开始取代 CRA。

感受: 这一步和 Vue 3 引入 Composition API 很像,大家还在写 SPA(单页应用)。

1. 状态管理:从“对象”到“钩子”

在旧版 React 中,状态必须放在 this.state 对象里;而现代 React 则通过 useState 这种“钩子”来按需声明。

js
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        Count: {this.state.count}
      </button>
    );
  }
}

Vue 视角: 这就像把 data() 里的属性变成了 ref()

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

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}
2. 副作用:从“生命周期”到“同步机制”

这是 React 变化最激进的地方。旧版像 Vue 2 一样分阶段(挂载、更新、卸载);新版则统一用 useEffect 来表达“当数据变化时,我要同步做某事”。

js
class DataFetcher extends React.Component {
  componentDidMount() {
    console.log("挂载:发请求");
  }

  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      console.log("更新:ID 变了,重新发请求");
    }
  }

  componentWillUnmount() {
    console.log("卸载:清理定时器");
  }
}

Vue 视角: useEffect 一个人干了 mountedupdatedunmounted 三个人的活,逻辑类似于 watch 加了 immediate: true

js
function DataFetcher({ id }) {
  useEffect(() => {
    console.log("执行:不论是初次还是 ID 变了,我都会运行");

    return () => {
      console.log("清理:在下一次运行前或卸载时执行(类似 cleanup)");
    };
  }, [id]); // 只有 id 变了才重新执行
}
阶段三:全栈/服务器组件时代 (2023-至今) —— 现在的 React 官网

关键词: Server Components (RSC), Next.js, Full-stack.

路径: 直接 Next.js。

为什么这么激进? React 团队认为,在浏览器端(Client)折腾性能已经到头了。要解决加载慢、打包体积大,必须让一部分代码死在服务器上

后果: 这导致了你发现的“断层”。官方不再教你如何从零组装一个 React App,而是直接给你一个 Next.js 这种“全栈怪兽”。

3. 给你规划一条 React 的“平滑”学习路径

如果你想像学 Vue 那样踏实地进步,我建议你忽略官网的首屏推荐,按照以下路径走:

第一步:核心基础(对应 Vue 基础)

工具: 使用 Vite 创建一个最简单的 React 项目(npm create vite@latest)。

重点: 掌握 JSX 语法、useStateuseEffect

目标: 能用 React 写出和 Vue 一样功能的单页面组件。

第二步:生态组装(对应 Vue Router + Pinia)

路由: 学习 React Router(最基础的路由跳转)。

状态: 学习 Zustand(对应 Pinia 的简洁体验)。

目标: 能够自己用 Vite + React Router + Zustand 拼装出一个后台管理系统。

第三步:全栈进阶(对应 Nuxt.js)

工具: 开启 Next.jsReact Router v7 (Framework mode)

重点: 学习服务端渲染 (SSR)、数据预取、服务端组件。

目标: 理解为什么代码要分“服务端”和“客户端”运行。

为什么 React 没有“官方脚手架”了?

Vue 有 Vue CLI 到现在的 create-vue,一直是官方维护。 React 原来的官方脚手架叫 CRA (Create React App),但由于它基于 Webpack 且更新极其缓慢,导致性能被 Vite 甩开几条街。

React 团队面临两个选择:

  1. 花大力气重写一个基于 Vite 的官方脚手架。
  2. “借刀杀人”: 承认单页应用(SPA)有局限性,把 Next.js 这种现成的成熟方案扶正为“官方推荐”。

React 选择了后者。 这对开发者来说,虽然省去了配置工具链的麻烦,但确实造成了“还没学会走就得学跑”的

当你运行 npm create vite@latest 时,你调用的不是 React 团队的代码,而是 Vite 团队(主要由 Vue 作者尤雨溪及其社区维护) 编写的模板。

  • Vite 团队的想法: “我们要做一个通用的、飞快的构建工具。我们要为所有主流框架(Vue, React, Svelte, Preact)都提供基础模板。”
  • React 团队的想法: “我们不再维护基础模板了,因为我们要推行『全栈框架』理念。如果你想写纯 React,你自己找工具吧(比如去用 Vite)。”

所以,React 官方不推荐,不代表 React 不能用 Vite。 事实上,全世界绝大多数的 React 开发者在写非全栈项目时,用的都是 Vite。

快速入门

创建和嵌套组件

React 应用程序是由 组件 组成的。一个组件是 UI(用户界面)的一部分,它拥有自己的逻辑和外观。组件可以小到一个按钮,也可以大到整个页面。

React 组件是返回标签的 JavaScript 函数:

jsx
function MyButton() {
  return (
    <button>我是一个按钮</button>
  );
}

括号是用于换行,也可以直接返回

jsx
function MyButton() {
  	return <button>我是一个按钮</button>;
}

至此,你已经声明了 MyButton,现在把它嵌套到另一个组件中:

jsx
export default function MyApp() {
  return (
    <div>
      <h1>欢迎来到我的应用</h1>
      <MyButton />
    </div>
  );
}

你可能已经注意到 <MyButton /> 是以大写字母开头的。你可以据此识别 React 组件。React 组件必须以大写字母开头,而 HTML 标签则必须是小写字母。

jsx
function MyButton() {
  return (
    <button>
      我是一个按钮
    </button>
  );
}

export default function MyApp() {
  return (
    <div>
      <h1>欢迎来到我的应用</h1>
      <MyButton />
    </div>
  );
}

export default 关键字指定了文件中的主要组件。

使用 JSX 编写标签

上面所使用的标签语法被称为 JSX。它是可选的,但大多数 React 项目会使用 JSX,主要是它很方便。

JSX 比 HTML 更加严格。你必须闭合标签,如 <br />。你的组件也不能返回多个 JSX 标签。你必须将它们包裹到一个共享的父级中,比如 <div>...</div> 或使用空的 <>...</> 包裹:

jsx
function AboutPage() {
  return (
    <>
      <h1>关于</h1>
      <p>你好。<br />最近怎么样?</p>
    </>
  );
}

添加样式

在 React 中,你可以使用 className 来指定一个 CSS 的 class。它与 HTML 的 class 属性的工作方式相同:

jsx
<img className="avatar" />

然后,你可以在一个单独的 CSS 文件中为它编写 CSS 规则:

jsx
/* 在你的 CSS 文件中修改 */
.avatar {
  border-radius: 50%;
}

React 并没有规定你如何添加 CSS 文件。最简单的方式是使用 HTML 的 <link> 标签。如果你使用了构建工具或框架,请阅读其文档来了解如何将 CSS 文件添加到你的项目中。

显示数据

JSX 会让你把标签放到 JavaScript 中。而大括号会让你 “回到” JavaScript 中,这样你就可以从你的代码中嵌入一些变量并展示给用户。例如,这将显示 user.name

jsx
return (
  <h1>
    {user.name}
  </h1>
);

你还可以将 JSX 属性 “转义到 JavaScript”,但你必须使用大括号 而非 引号。例如,className="avatar" 是将 "avatar" 字符串传递给 className,作为 CSS 的 class。但 src={user.imageUrl} 会读取 JavaScript 的 user.imageUrl 变量,然后将该值作为 src 属性传递:

jsx
return (
  <img
    className="avatar"
    src={user.imageUrl}
  />
);

你也可以把更为复杂的表达式放入 JSX 的大括号内,例如 字符串拼接:

jsx
const user = {
  name: 'Hedy Lamarr',
  imageUrl: 'https://react.dev/images/docs/scientists/yXOvdOSs.jpg',
  imageSize: 90,
};

export default function Profile() {
  return (
    <>
      <h1>{user.name}</h1>
      <img
        className="avatar"
        src={user.imageUrl}
        alt={'Photo of ' + user.name}
        style={{
          width: user.imageSize,
          height: user.imageSize
        }}
      />
    </>
  );
}

在上面示例中,style={{}} 并不是一个特殊的语法,而是 style={ } JSX 大括号内的一个普通 {} 对象。当你的样式依赖于 JavaScript 变量时,你可以使用 style 属性。

条件渲染

React 没有特殊的语法来编写条件语句,因此你使用的就是普通的 JavaScript 代码。例如使用 if 语句根据条件引入 JSX:

jsx
let content;
if (isLoggedIn) {
  content = <AdminPanel />;
} else {
  content = <LoginForm />;
}
return (
  <div>
    {content}
  </div>
);

如果你喜欢更为紧凑的代码,可以使用 条件 ? 运算符。与 if 不同的是,它工作于 JSX 内部:

jsx
<div>
  {isLoggedIn ? (
    <AdminPanel />
  ) : (
    <LoginForm />
  )}
</div>

当你不需要 else 分支时,你也可以使用更简短的 逻辑 && 语法:

jsx
<div>
  {isLoggedIn && <AdminPanel />}
</div>

所有这些方法也适用于有条件地指定属性。如果你对 JavaScript 语法不熟悉,你可以先使用 if...else

渲染列表

你将依赖 JavaScript 的特性,例如 for 循环 和 array 的 map() 函数 来渲染组件列表。

假设你有一个产品数组:

jsx
const products = [
  { title: 'Cabbage', id: 1 },
  { title: 'Garlic', id: 2 },
  { title: 'Apple', id: 3 },
];

在你的组件中,使用 map() 函数将这个数组转换为 <li> 标签构成的列表:

jsx
const listItems = products.map(product =>
  <li key={product.id}>
    {product.title}
  </li>
);

return (
  <ul>{listItems}</ul>
);

注意, <li> 有一个 key 属性。对于列表中的每一个元素,你都应该传递一个字符串或者数字给 key,用于在其兄弟节点中唯一标识该元素。通常 key 来自你的数据,比如数据库中的 ID。如果你在后续插入、删除或重新排序这些项目,React 将依靠你提供的 key 来思考发生了什么。

响应事件

你可以通过在组件中声明 事件处理 函数来响应事件:

jsx
function MyButton() {
  function handleClick() {
    alert('You clicked me!');
  }
  return (
    <button onClick={handleClick}>
      点我
    </button>
  );
}

注意,onClick={handleClick} 的结尾没有小括号!不要 调用 事件处理函数:你只需 把函数传递给事件 即可。当用户点击按钮时 React 会调用你传递的事件处理函数。

更新界面

通常你会希望你的组件 “记住” 一些信息并展示出来,比如一个按钮被点击的次数。要做到这一点,你需要在你的组件中添加 state

首先,从 React 引入 useState

jsx
import { useState } from 'react';

现在你可以在你的组件中声明一个 state 变量

jsx
function MyButton() {
  const [count, setCount] = useState(0);
  // ...

你将从 useState 中获得两样东西:当前的 state(count),以及用于更新它的函数(setCount)。你可以给它们起任何名字,但按照惯例会像 [something, setSomething] 这样为它们命名。

第一次显示按钮时,count 的值为 0,因为你把 0 传给了 useState()。当你想改变 state 时,调用 setCount() 并将新的值传递给它。点击该按钮计数器将递增:

jsx
function MyButton() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(count + 1);
  }
  return (
    <button onClick={handleClick}>
      Clicked {count} times
    </button>
  );
}

React 将再次调用你的组件函数。第一次 count 变成 1。接着点击会变成 2。继续点击会逐步递增。

如果你多次渲染同一个组件,每个组件都会拥有自己的 state。你可以尝试点击不同的按钮:

jsx
import { useState } from 'react';

export default function MyApp() {
  return (
    <div>
      <h1>独立更新的计数器</h1>
      <MyButton />
      <MyButton />
    </div>
  );
}

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

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      点了 {count} 次
    </button>
  );
}

注意,每个按钮会 “记住” 自己的 count,而不影响其他按钮。

使用 Hook

use 开头的函数被称为 HookuseState 是 React 提供的一个内置 Hook。你可以在 React API 参考 中找到其他内置的 Hook。你也可以通过组合现有的 Hook 来编写属于你自己的 Hook。

Hook 比普通函数更为严格。你只能在你的组件(或其他 Hook)的 顶层 调用 Hook。如果你想在一个条件或循环中使用 useState,请提取一个新的组件并在组件内部使用它。

组件间共享数据

在前面的示例中,每个 MyButton 都有自己独立的 count,当每个按钮被点击时,只有被点击按钮的 count 才会发生改变:

image-20260506183209200

然而,你经常需要组件 共享数据并一起更新

为了使得 MyButton 组件显示相同的 count 并一起更新,你需要将各个按钮的 state “向上” 移动到最接近包含所有按钮的组件之中。

在这个示例中,它是 MyApp

image-20260506183241021此刻,当你点击任何一个按钮时,MyApp 中的 count 都将改变,同时会改变 MyButton 中的两个 count。具体代码如下:

首先,将 MyButtonstate 上移到 MyApp 中:

jsx
export default function MyApp() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(count + 1);
  }
  return (
    <div>
      <h1>独立更新的计数器</h1>
      <MyButton />
      <MyButton />
    </div>
  );
}

function MyButton() {

  // ... 我们正在从这里移动代码...

}

接着,将 MyApp 中的点击事件处理函数以及 state 一同向下传递到 每个 MyButton 中。你可以使用 JSX 的大括号向 MyButton 传递信息。就像之前向 <img> 等内置标签所做的那样:

jsx
export default function MyApp() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(count + 1);
  }

  return (
    <div>
      <h1>共同更新的计数器</h1>
      <MyButton count={count} onClick={handleClick} />
      <MyButton count={count} onClick={handleClick} />
    </div>
  );

}

使用这种方式传递的信息被称作 prop。此时 MyApp 组件包含了 count state 以及 handleClick 事件处理函数,并将它们作为 prop 传递给 了每个按钮。

最后,改变 MyButton读取 从父组件传递来的 prop:

jsx
function MyButton({ count, onClick }) {
  return (
    <button onClick={onClick}>
      点了 {count} 次
    </button>
  );
}

当你点击按钮时,onClick 处理程序会启动。每个按钮的 onClick prop 会被设置为 MyApp 内的 handleClick 函数,所以函数内的代码会被执行。该代码会调用 setCount(count + 1),使得 state 变量 count 递增。新的 count 值会被作为 prop 传递给每个按钮,因此它们每次展示的都是最新的值。这被称为“状态提升”。通过向上移动 state,我们实现了在组件间共享它。

jsx
import { useState } from 'react';

export default function MyApp() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <div>
      <h1>共同更新的计数器</h1>
      <MyButton count={count} onClick={handleClick} />
      <MyButton count={count} onClick={handleClick} />
    </div>
  );
}

function MyButton({ count, onClick }) {
  return (
    <button onClick={onClick}>
      点了 {count} 次
    </button>
  );
}

安装

创建一个 React 应用

如果你想用 React 构建一个新的应用或网站,我们推荐从一个框架开始。

如果你的应用程序具有现有框架无法很好满足的约束,你更喜欢构建自己的框架,或者你只想学习 React 应用程序的基础知识,你可以 从头开始构建 React 应用

全栈框架

这些推荐的框架支持你在生产环境中部署和扩展应用所需的所有功能。它们集成了 React 的最新特性,并充分利用了 React 的架构。

Next.js (App Router)

Next.js 的 App Router 是一个 React 框架,充分利用了 React 的架构,支持全栈 React 应用。

npx create-next-app@latest

Next.js 由 Vercel 维护。你可以 将 Next.js 应用部署 到任何支持 Node.js 或 Docker 容器的托管平台,或者部署到你自己的服务器。 Next.js 也支持 静态导出 无需服务器。

React Router (v7)

React Router 是 React 最流行的路由库,可以与 Vite 结合创建一个全栈 React 框架。它强调标准的 Web API 并提供了多个 可部署的模板 适用于各种 JavaScript 运行时和平台。

要创建一个新的 React Router 框架项目,请运行:

shell
npx create-react-router@latest

React Router 由 Shopify 维护。

Expo (for native apps)

Expo 是一个 React 框架,让你可以创建支持真正原生 UI 的通用 Android、iOS 和 Web 应用。它为 React Native 提供了一个 SDK,让原生部分更易于使用。要创建一个新的 Expo 项目,请运行:

bash
npx create-expo-app@latest

如果你是 Expo 新手,请查看 Expo 教程.

从零开始构建 React 应用

如果现有框架无法满足你应用的要求,或者你更喜欢自己构建框架,亦或者**你只想学习 React 应用程序的基础知识**,那么你可以选择从零开始构建 React 应用。

步骤 1: 安装构建工具

第一步是安装一个构建工具,如 Vite、Parcel 或 rsbuild。这些构建工具提供了打包和运行源代码的功能,提供本地开发的开发服务器,以及部署应用到生产服务器的构建命令。

Vite

Vite 是一个构建工具,旨在为现代网络项目提供更快更简洁的开发体验。

npm create vite@latest my-app -- --template react-ts
或者使用:
npm create vite@latest
然后跟着索引选择

Vite 采用约定式设计,开箱即提供合理的默认配置。它拥有丰富的插件生态系统,能够支持快速热更新、JSX、Babel/SWC 等常见功能。你可以查看 Vite 的 React 插件React SWC 插件React 服务器端渲染示例项目 来开始使用。

Vite 已经作为构建工具在我们 推荐的框架 之一 React Router 中使用。

Parcel

Parcel 结合了出色的开箱即用开发体验和可扩展的架构,可以将你的项目从刚开始的阶段推向大规模的生产应用。

npm install --save-dev parcel

Parcel 支持快速刷新、JSX、TypeScript、Flow 和开箱即用的样式。请查看 Parcel 的 React 教程 以开始。

Rsbuild

Rsbuild 是一个基于 Rspack 的构建工具,旨在为 React 应用程序提供无缝的开发体验。它配备了精心调优的默认设置和现成的性能优化。

npx create-rsbuild --template react

Rsbuild 内置了对 React 特性的支持,如快速刷新、JSX、TypeScript 和样式。请查看 Rsbuild 的 React 指南 以开始使用。

React Native 的 Metro

如果你从头开始使用 React Native,你将需要使用 Metro, 这是 React Native 的 JavaScript 打包工具。Metro 支持 iOS 和 Android 等平台的打包,但与这里提到的工具相比,它缺少许多功能。除非你的项目需要 React Native 支持,否则我们建议从 Vite、Parcel 或 Rsbuild 开始。

步骤 2: 构建常见的应用程序模式

上面列出的构建工具从客户端单页应用程序(SPA)开始,但不包括路由、数据获取或样式等常见功能的进一步解决方案。

React 生态系统中包含许多用于解决这些问题的工具。我们列出了一些广泛使用的工具作为起点,但如果其他工具更适合你,欢迎选择使用。

路由

路由决定了当用户访问特定 URL 时显示的内容或页面。你需要设置一个路由来将 URL 映射到应用程序的不同部分。你还需要处理嵌套路由、路由参数和查询参数。路由可以在代码中配置,也可以根据组件文件夹和文件结构定义。

路由是现代应用程序的核心部分,通常与数据获取(包括为整个页面预取数据以加快加载速度)、代码拆分(以最小化客户端包的大小)和页面渲染方法(决定每个页面的生成方式)集成在一起。

我们建议使用:

数据获取

从服务器或其他数据源获取数据是大多数应用程序的关键部分。要正确地执行此操作,需要处理加载状态、错误状态以及缓存获取的数据,这可能会很复杂。

专门构建的数据获取库为你完成数据获取和缓存的繁重工作,让你专注于应用程序需要哪些数据以及如何显示这些数据。这些库通常直接在组件中使用,但也可以集成到路由加载器中,以实现更快的预取和更好的性能,并且也可以用于服务器渲染。

请注意,直接在组件中获取数据可能会导致加载时间变慢,因为会出现网络请求瀑布效应,因此我们建议尽可能在路由加载器或服务器上预取数据!这样可以在页面显示时一次性获取页面的数据。

如果你从大多数后端或 REST 风格的 API 获取数据,我们建议使用:

如果你从 GraphQL API 获取数据,我们建议使用:

代码拆分

代码拆分是将应用程序分解为可以按需加载的小型包的过程。随着每个新功能和额外依赖的增加,应用程序的代码体积会增大。应用程序可能会因为需要在使用前发送整个应用程序的所有代码而变得加载缓慢。缓存、减少功能/依赖项以及将部分代码移至服务器运行可以帮助缓解加载缓慢的问题,但如果过度使用,这些都是不完整的解决方案,可能会牺牲功能。

同样,如果你依赖使用框架的应用程序来拆分代码,可能会遇到加载速度比不进行代码拆分时更慢的情况。例如,懒加载 图表会延迟发送渲染图表所需的代码,将图表代码与应用程序的其余部分分开。Parcel 支持使用 React.lazy 进行代码拆分。然而,如果图表在初始渲染后才加载其数据,你就需要等待两次。这就是所谓的瀑布效应:与其同时获取图表的数据和发送渲染代码,你必须等待每个步骤依次完成。

通过路由拆分代码,并与打包和数据获取集成,可以减少应用程序的初始加载时间以及渲染应用程序最大可见内容所需的时间 (最大内容绘制)。

有关代码拆分的说明,请参阅你的构建工具文档:

提高应用程序性能

由于你选择的构建工具仅支持单页应用程序(SPA),你需要实现其他 渲染模式 如服务器端渲染(SSR)、静态站点生成(SSG)和/或 React 服务器组件(RSC)。即使你一开始不需要这些功能,将来也可能有一些路由会从 SSR、SSG 或 RSC 中受益。

  • 单页面应用程序 (SPA) 加载单个 HTML 页面,并在用户与应用程序交互时动态更新页面。SPA 更容易入门,但初始加载时间可能较慢。SPA 是大多数构建工具的默认架构。
  • 流式服务器端渲染 (SSR) 在服务器上渲染页面并将完全渲染的页面发送到客户端。SSR 可以提高性能,但设置和维护比单页应用程序更复杂。随着流式处理的加入,SSR 的设置和维护可能会变得非常复杂。 请参阅 Vite 的 SSR 指南
  • 静态站点生成 (SSG) 在构建时为你的应用生成静态 HTML 文件。SSG 可以提高性能,但设置和维护可能比服务器端渲染更复杂。请参阅Vite 的 SSG 指南
  • React 服务器组件 (RSC) 允许你在单个 React 树中混合构建时、仅服务器和交互式组件。RSC 可以提高性能,但目前需要深入的专业知识来设置和维护。请参阅 Parcel 的 RSC 示例

你的渲染策略需要与路由集成,以便使用你的框架构建的应用程序可以在每个路由级别选择渲染策略。这将使你能够在不重写整个应用程序的情况下使用不同的渲染策略。例如,你的应用程序的登录页面可能会从静态生成 (SSG) 中受益,而具有内容提要的页面可能在服务器端渲染时表现最佳。

使用合适的渲染策略针对合适的路由可以减少加载第一个内容字节的时间 (首字节时间),第一个内容元素渲染的时间 (首次内容绘制),以及应用程序最大可见内容渲染的时间 (最大内容绘制)。

将 React 添加到现有项目中

如果想对现有项目添加一些交互,不必使用 React 将其整个重写。只需将 React 添加到已有技术栈中,就可以在任何位置渲染交互式的 React 组件。

在现有网站的子路由中使用 React

假设你在 example.com 部署了一个其他服务端技术(例如 Rails)构建的 Web 应用,但是你又想在 example.com/some-app/ 部署一个 React 项目。

以下是推荐的配置方式:

  1. 使用一个 基于 React 的框架 构建 应用的 React 部分
  2. 在框架配置中将 /some-app 指定为基本路径
  3. 配置服务器或代理,以便所有位于 /some-app/ 下的请求都由 React 应用处理。

许多基于 React 的框架都是全栈的,从而可以让你的 React 应用充分利用服务器。但是,即使无法或不想在服务器上运行 JavaScript,也可以使用相同的方法。在这种情况下,将 HTML/CSS/JS 导出(Next.js 的 next export output,Gatsby 的 default)替换为 /some-app/

在现有页面的一部分中使用 React

假设有一个其他技术栈(无论是 Rails 这样的服务端技术,还是 Backbone 那样的客户端技术)构建的现有页面,并且想要在该页面的某个位置渲染交互式的 React 组件。这是集成 React 的常见方式——实际上,这也正是多年来大多数情况下 Meta 使用 React 的方式!

你可以分两步进行:

  1. 配置 JavaScript 环境,以便使用 JSX 语法、importexport 语法将代码拆分为模块,以及从 npm 包注册表中使用包(例如 React)。
  2. 在需要的位置渲染 React 组件

确切的方法取决于现有的页面配置,因此让我们对一些细节进行说明。

步骤 1:配置模块化的 JavaScript 环境

模块化的 JavaScript 环境可以让你在单一的文件中编写 React 组件,而不是在一个文件中编写所有的代码。它还可以让你使用其他开发人员在 npm 注册表上发布的一些特别好用的包,包括 React!如何实现这一点取决于你现有的配置:

  • 如果你的应用已经使用 import 语句来分割成不同的文件,请尝试利用已有的配置。检查在你的 JavaScript 代码中编写 <div />是否会导致语法错误。
  • 如果你的应用没有用于编译 JavaScript 模块的配置,请使用 Vite 进行配置。Vite 社区维护了与后端框架(包括 Rails、Django 和 Laravel)的 许多集成项目。如果你的后端框架没有列出,请 按照此指南 手动将 Vite 构建集成到你的后端。

如果想要检查你的配置是否有效,可以在项目文件夹中运行以下命令:

bash
npm install react react-dom

然后在你的 JavaScript 主文件(它可能被称为 index.jsmain.js)的顶部添加以下代码:

jsx
import { createRoot } from 'react-dom/client';

// 清除现有的 HTML 内容
document.body.innerHTML = '<div id="app"></div>';

// 渲染你的 React 组件
const root = createRoot(document.getElementById('app'));
root.render(<h1>Hello, world</h1>);

如果页面的全部内容都被替换为“Hello, world!”,则一切正常!那么继续阅读。

步骤 2:在页面的任何位置渲染 React 组件

在上一步中,此代码将被放在主文件的顶部:

jsx
import { createRoot } from 'react-dom/client';

// 清除现有的 HTML 内容
document.body.innerHTML = '<div id="app"></div>';
// 渲染你的 React 组件
const root = createRoot(document.getElementById('app'));
root.render(<h1>Hello, world</h1>);

当然,你实际上并不想清除现有的 HTML 内容!

那么请删除此代码。

相反,你可能想要在 HTML 中特定的位置渲染 React 组件。打开 HTML 页面(或用于生成它的服务端模板),并向任意一个标签添加一个唯一的 id 属性,例如:

jsx
<!-- ... 你的 HTML 代码某处 ... -->
<nav id="navigation"></nav>
<!-- ... 其他 HTML 代码 ... -->

这样可以使用 document.getElementById 查找到该 HTML 元素,并将其传递给 createRoot,以便可以在其中渲染自己的 React 组件:

jsx
import { createRoot } from 'react-dom/client';

function NavigationBar() {
  // TODO: 实际实现一个导航栏
  return <h1>Hello from React!</h1>;
}

const domNode = document.getElementById('navigation');
const root = createRoot(domNode);
root.render(<NavigationBar />);

请注意 index.html 中的原始 HTML 内容是如何保留的,但现在你自己的 NavigationBar React 组件出现在 HTML 的 <nav id="navigation"> 中。阅读 createRoot 用法文档 以了解如何在现有 HTML 页面中渲染 React 组件。

当在现有项目中采用 React 时,通常会从小型交互式组件(例如按钮)开始,然后逐渐“向上移动”,直到最终整个页面都由 React 构建。到那个时候,我们建议立即迁移到 一个 React 框架,以充分利用 React 的优势。

在现有的原生移动应用中使用 React Native

React Native 也可以逐步集成到现有的原生应用中。如果已经有一个现有的 Android(Java 或 Kotlin)或 iOS(Objective-C 或 Swift)原生应用,请 按照本指南 将 React Native 添加到其中。

编辑器设置

正确的编辑器配置可以开发事半功倍。并且还可以在编码时帮你提示错误!如果这是你第一次配置编辑器,或者想对现有编辑器进行调整,我们有一些建议可供参考。

编辑器配置

VS Code 是现如今最流行的编辑器之一。它拥有庞大的扩展市场,同时可以与 GitHub 等流行服务完美集成。下面列出的大多数功能都可以作为扩展添加到 VS Code 中,使其具有极高的可配置性!

代码检查(Linting)

代码检查(Code linters)可以在你编写代码时,发现代码中的问题,以帮你尽早修复。ESLint 是一款流行且开源的 JavaScript 代码检查工具。

请确保你已经为你的项目启用了 eslint-plugin-react-hooks 规则。这在 React 项目中是必备的,同时能帮助你及早的捕获较为严重的 bug。我们推荐的 eslint-config-react-app preset 中已经集成了该规则。

格式化

与其他贡献者共享代码时,你最不想做的事就是争论代码缩进应该使用 tabs 还是空格!幸好,Prettier 会根据预设配置的规则重新格式化代码,以保证代码整洁。运行 Prettier,你的所有 tabs 都将转换为空格,同时缩进、引号等也都将根据你的配置而改变。理想状态下,当你保存文件时,Prettier 会自动执行格式化操作。

你可以为 VSCode 安装 Prettier 扩展,具体步骤如下:

  1. 启动 VS Code
  2. 使用快速打开(使用快捷键 Ctrl/Cmd + P
  3. 粘贴 ext install esbenp.prettier-vscode
  4. 按回车键

如果你的 ESLint 预设包含格式化规则,它们可能会与 Prettier 发生冲突。我们建议使用eslint-config-prettier 禁用你 ESLint 预设中的所有格式化规则,这样 ESLint 就只用于捕捉逻辑错误。如果你想在合并 PR 前强制执行文件的格式化,请在你的 CI 中使用 prettier --check 命令。

使用 TypeScript

TypeScript 是一种向 JavaScript 代码添加类型定义的常用方法。TypeScript 天然支持 JSX——只需在项目中添加 @types/react@types/react-dom 即可获得完整的 React Web 支持。

安装

在现有 React 项目中添加 TypeScript

使用下面命令安装最新版本的 React 类型定义:

npm install --save-dev @types/react @types/react-dom

然后在 tsconfig.json 中设置以下编译器选项:

  1. 必须在 lib 中包含 dom(注意:如果没有指定 lib 选项,默认情况下会包含 dom)。
  2. jsx 必须设置为一个有效的选项。对于大多数应用程序,preserve 应该足够了。 如果你正在发布一个库,请查阅 jsx 文档 以选择合适的值。

在 React 组件中使用 TypeScript

每个包含 JSX 的文件都必须使用 .tsx 文件扩展名。这是一个 TypeScript 特定的扩展,告诉 TypeScript 该文件包含 JSX。

使用 TypeScript 编写 React 与使用 JavaScript 编写 React 非常相似。与组件一起工作时的关键区别是,你可以为组件的 props 提供类型。这些类型可用于正确性检查,并在编辑器中提供内联文档。

以 快速入门 指南中的 MyButton 组件 为例,我们可以为按钮的 title 添加一个描述类型:

App.tsx

tsx
function MyButton({ title }: { title: string }) {
  return (
    <button>{title}</button>
  );
}

export default function MyApp() {
  return (
    <div>
      <h1>欢迎来到我的应用</h1>
      <MyButton title="我是一个按钮" />
    </div>
  );
}

这种内联语法是为组件提供类型的最简单方法,但是一旦你开始描述几个字段,它可能变得难以管理。相反,你可以使用 interfacetype 来描述组件的 props:

tsx
interface MyButtonProps {
  /** 按钮文字 */
  title: string;
  /** 按钮是否禁用 */
  disabled: boolean;
}

function MyButton({ title, disabled }: MyButtonProps) {
  return (
    <button disabled={disabled}>{title}</button>
  );
}

export default function MyApp() {
  return (
    <div>
      <h1>Welcome to my app</h1>
      <MyButton title="我是一个禁用按钮" disabled={true}/>
    </div>
  );
}

描述组件 props 的类型可以根据需要变得简单或复杂,但它们应该是使用 typeinterface 描述的对象类型。你可以在 对象类型 中了解 TypeScript 如何描述对象,但你可能还对使用 联合类型 描述可以是几种不同类型之一的 prop,以及在 从类型创建类型 指南中参考更高级的用例。

Hooks 示例

来自 @types/react 的类型定义包括内置的 Hook,因此你可以在组件中使用它们,无需任何额外设置。它们是根据你在组件中编写的代码构建的,所以你会得到很多类型推断,并且理想情况下不需要处理提供类型的细节。

但是,我们可以看一下如何为 Hook 提供类型的几个示例。

useState

useState Hook 会重用作为初始 state 传入的值以确定值的类型。例如:

tsx
// 推断类型为 "boolean"
const [enabled, setEnabled] = useState(false);

这将为 enabled 分配 boolean 类型,而 setEnabled 将是一个接受 boolean 参数的函数,或者返回 boolean 的函数。如果你想为 state 显式提供一个类型,你可以通过为 useState 调用提供一个类型参数来实现:

tsx
// 显式设置类型为 "boolean"
const [enabled, setEnabled] = useState<boolean>(false);

在这种情况下,这并不是很有用,但是当你有一个联合类型时,你可能想要提供一个 type。例如,这里的 status 可以是几个不同的字符串之一:

tsx
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");

或者,你可以将相关的 state 作为一个对象分组,并通过对象类型描述不同的可能性:

tsx
type RequestState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success', data: any }
  | { status: 'error', error: Error };
const [requestState, setRequestState] = useState<RequestState>({ status: 'idle' });

useReducer

useReducer 是一个更复杂的 Hook,它接受一个 reducer 函数和一个初始 state 作为参数,并将从初始 state 推断出 reducer 函数的类型。你可以选择性地为 useReducer 提供类型参数以为 state 提供类型。但是更好的做法仍然是在初始 state 上添加类型:

tsx
import {useReducer} from 'react';

interface State {
   count: number
};

type CounterAction =
  | { type: "reset" }
  | { type: "setCount"; value: State["count"] }

const initialState: State = { count: 0 };

function stateReducer(state: State, action: CounterAction): State {
  switch (action.type) {
    case "reset":
      return initialState;
    case "setCount":
      return { ...state, count: action.value };
    default:
      throw new Error("Unknown action");
  }
}

export default function App() {
  const [state, dispatch] = useReducer(stateReducer, initialState);

  const addFive = () => dispatch({ type: "setCount", value: state.count + 5 });
  const reset = () => dispatch({ type: "reset" });

  return (
    <div>
      <h1>欢迎来到我的计数器</h1>

      <p>计数: {state.count}</p>
      <button onClick={addFive}>加 5</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

我们在几个关键位置使用了 TypeScript:

  • interface State 描述了 reducer state 的类型。
  • type CounterAction 描述了可以 dispatch 至 reducer 的不同 action。
  • const initialState: State 为初始 state 提供类型,并且也将成为 useReducer 默认使用的类型。
  • stateReducer(state: State, action: CounterAction): State 设置了 reducer 函数参数和返回值的类型。

除了在 initialState 上设置类型外,一个更明确的替代方法是为 useReducer 提供一个类型参数:

tsx
import { stateReducer, State } from './your-reducer-implementation';
const initialState = { count: 0 };
export default function App() {
  const [state, dispatch] = useReducer<State>(stateReducer, initialState);
}

useContext

useContext 是一种无需通过组件传递 props 而可以直接在组件树中传递数据的技术。它是通过创建 provider 组件使用,通常还会创建一个 Hook 以在子组件中使用该值。

从传递给 createContext 调用的值推断 context 提供的值的类型:

tsx
import { createContext, useContext, useState } from 'react';

type Theme = "light" | "dark" | "system";
const ThemeContext = createContext<Theme>("system");

const useGetTheme = () => useContext(ThemeContext);

export default function MyApp() {
  const [theme, setTheme] = useState<Theme>('light');

  return (
    <ThemeContext value={theme}>
      <MyComponent />
    </ThemeContext>
  )
}

function MyComponent() {
  const theme = useGetTheme();
  return (
    <div>
      <p>当前主题:{theme}</p>
    </div>
  )
}

当你有一个合理的默认值情况下,这种技术是有效的。但是当你没有合理的默认值的时候,null 作为默认值可能感觉是合理的。但是为了让类型系统理解你的代码,你需要在 createContext 上显式设置 ContextShape | null

这会导致一个问题,你需要在 context consumer 中消除 | null 的类型。我们建议让 Hook 在运行时检查它的存在,并在不存在时抛出一个错误:

tsx
import { createContext, useContext, useState, useMemo } from 'react';

// 这是一个简单的示例,但你可以想象一个更复杂的对象
type ComplexObject = {
  kind: string
};

// 上下文在类型中创建为 `| null`,以准确反映默认值。
const Context = createContext<ComplexObject | null>(null);

// 这个 Hook 会在运行时检查 context 是否存在,并在不存在时抛出一个错误。
const useGetComplexObject = () => {
  const object = useContext(Context);
  if (!object) { throw new Error("useGetComplexObject must be used within a Provider") }
  return object;
}

export default function MyApp() {
  const object = useMemo(() => ({ kind: "complex" }), []);

  return (
    <Context value={object}>
      <MyComponent />
    </Context>
  )
}

function MyComponent() {
  const object = useGetComplexObject();

  return (
    <div>
      <p>Current object: {object.kind}</p>
    </div>
  )
}

useMemo (记忆化一个值)

注意

React 编译器会自动对值和函数进行记忆化处理,从而减少手动调用 useMemo 的需求。你可以使用编译器自动处理记忆化。

useMemo 会从函数调用中创建/重新访问记忆化值,只有在第二个参数中传入的依赖项发生变化时,才会重新运行该函数。函数的类型是根据第一个参数中函数的返回值进行推断的,如果希望明确指定,可以为该 Hook 提供一个类型参数以指定函数类型。

tsx
// 从 filterTodos 的返回值推断 visibleTodos 的类型
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);

useCallback (记忆化一个函数)

React 编译器会自动对值和函数进行记忆化处理,从而减少手动调用 useCallback 的需求。你可以使用编译器自动处理记忆化。

useCallback 会在第二个参数中传入的依赖项保持不变的情况下,为函数提供相同的引用。与 useMemo 类似,函数的类型是根据第一个参数中函数的返回值进行推断的,如果希望明确指定,可以为这个 Hook 提供一个类型参数以指定函数类型。

tsx
const handleClick = useCallback(() => {
  // ...
}, [todos]);

当在 TypeScript 严格模式下,使用 useCallback 需要为回调函数中的参数添加类型注解。这是因为回调函数的类型是根据函数的返回值进行推断的——如果没有参数,那么类型就不能完全理解。

根据自身的代码风格偏好,你可以使用 React 类型中的 EventHandler 函数以在定义回调函数的同时为事件处理程序提供类型注解:

tsx
import { useState, useCallback } from 'react';

export default function Form() {

  const [value, setValue] = useState("Change me");
  const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((event) => {
    setValue(event.currentTarget.value);
  }, [setValue])
  return (
    <>
      <input value={value} onChange={handleChange} />
      <p>值: {value}</p>
    </>
  );
}

常用类型

当逐渐适应 React 和 TypeScript 的搭配使用后, 可以尝试阅读 @types/react,此库提供了一整套类型。你可以在 DefinitelyTyped 的 React 目录中 找到它们。我们将在这里介绍一些更常见的类型。

DOM 事件

在 React 中处理 DOM 事件时,事件的类型通常可以从事件处理程序中推断出来。但是,当你想提取一个函数以传递给事件处理程序时,你需要明确设置事件的类型。

tsx
import { useState } from 'react';

export default function Form() {
  const [value, setValue] = useState("Change me");

  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    setValue(event.currentTarget.value);
  }
  return (
    <>
      <input value={value} onChange={handleChange} />
      <p>值: {value}</p>
    </>
  );
}

React 类型中提供了许多事件类型 —— 完整列表可以在 这里 查看,它基于 DOM 的常用事件

当你需要确定某个类型时,可以先将鼠标悬停在你使用的事件处理器上,这样可以查看到事件的具体类型。

当你需要使用不包含在此列表中的事件时,你可以使用 React.SyntheticEvent 类型,这是所有事件的基类型。

子元素

描述组件的子元素有两种常见方法。第一种是使用 React.ReactNode 类型,这是可以在 JSX 中作为子元素传递的所有可能类型的并集:

tsx
interface ModalRendererProps {
  title: string;
  children: React.ReactNode;
}

这是对子元素的一个非常宽泛的定义。第二种方法是使用 React.ReactElement 类型,它**只包括 JSX 元素**,而不包括 JavaScript 原始类型,如 string 或 number:

tsx
interface ModalRendererProps {
  title: string;
  children: React.ReactElement;
}

注意,你不能使用 TypeScript 来描述子元素是某种类型的 JSX 元素,所以你不能使用类型系统来描述一个只接受 <li> 子元素的组件。

样式属性

当在 React 中使用内联样式时,你可以使用 React.CSSProperties 来描述传递给 style 属性的对象。这个类型是所有可能的 CSS 属性的并集,它能确保你传递给 style 属性的是有效的 CSS 属性,并且你能在编辑器中获得样式编码提示。

tsx
interface MyComponentProps {
  style: React.CSSProperties;
}

更多学习资源

本指南已经介绍了如何在 React 中使用 TypeScript 的基础知识,但还有更多内容等待学习。官网中的单个 API 页面或许包含了如何与 TypeScript 一起使用它们的更深入的说明。 文档中的各个 API 页面可能会包含更深入的说明,介绍如何在 TypeScript 中使用它们。

我们推荐以下资源:

React 开发者工具

使用 React 开发者工具检查 React components,编辑 propsstate,并识别性能问题。

你将会学习到

  • 如何安装 React 开发者工具

浏览器扩展

调试 React 构建的网站最简单的办法就是安装 React 开发者工具浏览器扩展。它可用于几种流行的浏览器:

现在,如果你访问一个用 React 构建 的网站,你将看到 ComponentsProfiler 面板。

image-20260506214705054

React 编译器

React 编译器是一个新的构建时工具,它可以自动优化你的 React 应用。它支持纯 JavaScript,并且了解 React 的规则,因此你无需重写任何代码即可使用它。

React 编译器会在构建时自动优化你的 React 应用。通常情况下,即使不进行优化,React 的性能也已经足够快,但有时你需要手动对组件和值进行记忆化(memoization)以保持应用的响应速度。这种手动记忆化既繁琐又容易出错,并且会增加需要维护的额外代码。React 编译器为你自动完成这些优化,减轻了你的思维负担,使你可以专注于功能的开发。

在使用 React 编译器之前

没有编译器的情况下,你需要手动对组件和值进行记忆化以优化重新渲染:

tsx
import { useMemo, useCallback, memo } from 'react';

const ExpensiveComponent = memo(function ExpensiveComponent({ data, onClick }) {
  const processedData = useMemo(() => {
    return expensiveProcessing(data);
  }, [data]);

  const handleClick = useCallback((item) => {
    onClick(item.id);
  }, [onClick]);

  return (
    <div>
      {processedData.map(item => (
        <Item key={item.id} onClick={() => handleClick(item)} />
      ))}
    </div>
  );
});

这种手动记忆化存在一个会破坏记忆化效果的细微 bug:

tsx
<Item key={item.id} onClick={() => handleClick(item)} />

尽管 handleClickuseCallback 包裹,但每次组件渲染时,箭头函数 () => handleClick(item) 都会创建一个新的函数。这意味着 <Item> 总会接收到一个新的 onClick prop,从而破坏了记忆化效果。

React 编译器无论是否存在这个箭头函数,都能够正确地进行优化,确保 Item 仅在 props.onClick 变化时才重新渲染。

在使用 React 编译器之后

使用 React 编译器,你可以编写相同的代码而无需手动进行记忆化:

tsx
function ExpensiveComponent({ data, onClick }) {
  const processedData = expensiveProcessing(data);

  const handleClick = (item) => {
    onClick(item.id);
  };

  return (
    <div>
      {processedData.map(item => (
        <Item key={item.id} onClick={() => handleClick(item)} />
      ))}
    </div>
  );
}

React 编译器会自动应用等效的优化,确保你的应用只在必要时重新渲染。

关于 useMemo、useCallback 和 React.memo 我应该怎么做?

默认情况下,React 编译器会根据其分析和启发式方法对你的代码进行记忆化处理。在大多数情况下,这种记忆化处理的效果会与你编写的代码一样精确,甚至更胜一筹。

然而,在某些情况下,开发者可能需要对记忆化进行更精细的控制。useMemouseCallback Hooks 可以继续与 React 编译器一起使用,作为一种脱围机制,用于控制哪些值会被记忆化。一个常见的用例是,如果记忆化的值被用作 effect 的依赖项,即使其依赖项没有实质性变化,effect 也不会反复触发。

对于增量代码,我们建议依赖编译器进行记忆化,并在需要时使用 useMemo/useCallback 以实现精确控制。

对于现有代码,我们建议要么保留现有的记忆化(移除它可能会改变编译输出),要么在移除记忆化之前进行仔细测试。

React 官方在教程的前半部分为了降低门槛,隐瞒了 React 的一个“先天缺陷”。现在到了进阶部分,它不得不把这个补丁掏出来。记忆化(Memoization)

简单来说,记忆化就是 “缓存”

在 React 里,只要父组件的状态变了,它的所有子组件默认都会重新运行一遍(Re-render)。 哪怕子组件的变量根本没变,React 也会傻傻地把子组件的代码从头执行到尾。

记忆化的目的: 告诉 React:“如果我的输入没变,就直接用上次算好的结果,别再跑一遍代码了。”

在 Vue 里,由于有响应式系统(依赖收集),Vue 知道只有用到那个变量的组件才需要变。React 没有这个系统,所以它只能靠“记忆化”来手动优化。

为什么突然冒出这么多 Hooks?

因为在 React Compiler 出现之前,开发者必须手动做缓存。

  • useMemo 缓存计算结果。类似于 Vue 的 computed
  • useCallback 缓存函数本身。防止每次渲染都生成一个新的函数引用。
  • React.memo 缓存整个组件。告诉 React:“除非 Props 变了,否则别重新渲染我。”
tsx
// 🔴 普通写法:每次父组件更新,这段沉重的计算都会重跑
const result = expensiveCalculation(data);
// 🟢 记忆化写法:只有 data 变了才重跑
const result = useMemo(() => expensiveCalculation(data), [data]);

React Compiler 是 React 团队承认“手动记忆化太痛苦”后的终极解决方案。

它是一个编译器(就像 Babel 或 Vite 里的插件),它的目标是:让你像写普通代码一样写 React,由它在编译阶段自动帮你加上 useMemouseCallback

安装

本指南将帮助你在 React 应用程序中安装和配置 React 编译器。

前提条件

React 编译器专为与 React 19 配合使用而设计,但也支持 React 17 和 18。了解有关 React 版本兼容性 的更多信息。

将 React 编译器安装为 devDependency

bash
npm install -D babel-plugin-react-compiler@latest

基本设置

React 编译器默认无需任何配置即可工作。不过,如果你需要在特殊情况下进行配置(例如,要支持低于 19 版本的 React),请参考编译器选项参考文档

设置过程**取决于你使用的构建工具**。React 编译器包含一个 Babel 插件,可以集成到你的构建流程中。

Babel

创建或更新你的 babel.config.js

js
module.exports = {
  plugins: [
    'babel-plugin-react-compiler', // 必须首先运行!
    // ... 其他插件
  ],
  // ... 其他配置
};

Vite 默认不读 babel.config.js,它的构建流程不经过 Babel(v6 之后用的是 oxc)。你就算创建了这个文件,Vite 也完全无视它。

babel.config.js 那种写法只适用于以 Babel 为核心构建工具的项目,比如用 Create React App 创建的老项目,或者直接用 @babel/core 驱动构建的项目。

Vite

如果你使用 Vite,@vitejs/plugin-react v6 已移除内置 Babel(改用 oxc),需要单独安装 @rolldown/plugin-babel

bash
npm install -D @rolldown/plugin-babel
ts
// vite.config.ts
import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'

export default defineConfig({
  plugins: [
    react(),
    babel({
      presets: [reactCompilerPreset()]
    })
  ],
})

⚠️ 注意:babel() 必须写在 react() 之后,否则编译器不生效。

babel-plugin-react-compiler 是 React 编译器本体,一个 Babel 插件,负责分析你的组件代码、自动插入 memoization 优化。它是”干活的”。

@rolldown/plugin-babel 是一个 Vite/Rolldown 的桥接层,负责让 Babel 插件能在 Vite 构建流程里跑起来。它是”搭桥的”,本身不做任何 React 相关的事。

Next.js

更多信息请参考 Next.js 文档

还有更多参照官网文档

ESLint 集成

React 编译器附带一条 ESLint 规则,用于识别无法被优化的代码。当它报错时,不代表整个项目出问题——编译器只会跳过那个特定组件或 Hook,其余部分照常优化。不需要立即修复所有问题,按自己节奏逐步处理即可。

安装:

bash
npm install -D eslint-plugin-react-hooks@latest

该规则包含在 recommended-latest 预设中。如果你还没配置过 eslint-plugin-react-hooks,参考 README 安装说明

启用后,ESLint 会:

  • 标出违反 React 规则 的代码
  • 指出哪些组件无法被优化,并给出修复提示

不装的话只是少了一层提示:当你写了违反 React 规则的代码导致某个组件无法被优化时,你不会收到任何警告,只是静默地那个组件没被优化到,你也不知道为什么。

装了之后 ESLint 会直接在编辑器里标红告诉你”这里有问题,编译器会跳过这个组件”,方便定位。

验证你的设置

安装后,请验证 React 编译器是否正常工作。

检查 React DevTools

由 React 编译器优化的组件会在 React DevTools 中显示一个 “Memo ✨” 徽章:

  1. 安装 React Developer Tools 浏览器扩展
  2. 在开发模式下打开你的应用
  3. 打开 React DevTools
  4. 查看组件名称旁边的 ✨ 表情符号

image-20260531010136015

如果编译器正在工作:

  • 组件将在 React DevTools 中显示一个 “Memo ✨” 徽章
  • 昂贵的计算将自动被记忆化
  • 无需手动使用 useMemo

故障排除

排除特定组件

如果某个组件在编译后引发问题,可以使用 "use no memo" 指令暂时将其排除:

function ProblematicComponent() {
  "use no memo";
  // 这里是组件代码
}

这会告诉编译器跳过对该特定组件的优化。你应该修复根本问题,并在解决后移除该指令。

image-20260531010301368

export default function App() {
  "use no memo";
  ....
}

如需更多故障排除帮助,请参阅调试指南

逐步使用

React 编译器可以逐步采用,允许你首先在代码库的特定部分尝试使用。本指南将向你展示如何在现有项目中逐步推广该编译器的使用。

为何采用渐进式迁移?

React 编译器的设计目的是自动优化你的整个代码库,但你不必一次性全部采用。渐进式采用让你能够控制推行过程,在扩展到其余部分之前,先在应用程序的小部分上测试编译器。

从小处着手有助于建立对编译器优化的信心。你可以验证应用在编译代码下的行为是否正确,测量性能提升,并识别代码库中的任何特定边缘情况。这种方法对于稳定性至关重要的生产应用程序尤其有价值。

渐进式采用还使得更容易处理编译器可能发现的任何违反 React 规则的问题。你可以在扩展编译器覆盖范围的同时有条不紊地解决这些问题,而不是一次性修复整个代码库中的违规问题。这使迁移过程更易于管理,并降低了引入错误的风险。

通过控制代码中哪些部分被编译,你还可以运行 A/B 测试以衡量编译器优化在实际应用中的效果。这些数据有助于你做出是否全面采用的明智决策,并向团队展示其价值。

渐进式采用的方法

有三种主要方法可以逐步采用 React 编译器:

  1. 配置插件范围 - 将编译器应用于特定目录
  2. 通过 “use memo” 选择加入 - 仅编译明确选择加入的组件

所有方法都允许你在完全上线之前,在应用程序的特定部分上测试该编译器。

vite配置插件范围

在 Vite 里可以用 reactCompilerPreset 的过滤选项实现。比如只编译特定目录:

ts
import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'

const preset = reactCompilerPreset()
// 只编译 src/modern 下的文件
preset.rolldown.filter.id.include = ['src/modern/**']

export default defineConfig({
  plugins: [
    react(),
    babel({ presets: [preset] })
  ],
})

或者用 compilationMode: 'annotation' 实现”选择加入”模式,只编译手动标注了 "use memo" 的组件:

ts
babel({
  presets: [reactCompilerPreset({ compilationMode: 'annotation' })]
})

使用 “use memo” 的选择加入模式

如需最大程度的控制,你可以使用 compilationMode: 'annotation',仅编译那些通过 "use memo" 指令显式选择加入的组件和 Hook。

这种方法可以让你对各个组件和挂钩进行细粒度的控制。当你希望在不影响整个目录的情况下,针对特定组件测试编译器时,这种方法非常有用。

annotation=注解

选择加入模式配置

vite.config.ts配置

ts
export default defineConfig({
  plugins: [react(),babel({
      presets: [reactCompilerPreset({ compilationMode: 'annotation' })]
    })],
})

选择加入指令

在要编译的函数开头添加 "use memo"

tsx
function TodoList({ todos }) {
  "use memo"; // 这个组件在编译里的选项
  const sortedTodos = todos.slice().sort();
  return (
    <ul>
      {sortedTodos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

function useSortedData(data) {
  "use memo"; // 这个组件在编译里的选项
  return data.slice().sort();
}

使用 compilationMode: 'annotation' 时,你必须:

  • 在每个需要优化的组件中添加 "use memo"
  • 在每个自定义 Hook 中添加 "use memo"
  • 记得在新组件中也添加它

这可以在你评估编译器影响的同时,精确控制哪些组件会被编译。

评论

评论加载中……