跳到主要内容

3 篇博文 含有标签「JavaScript」

查看所有标签

React 19 已于 2024 年 12 月 5 日正式发布,并在 npm 上稳定可用。这一版本带来了诸多令人振奋的新特性和改进,旨在简化开发流程、提升性能,并增强与现代开发需求的兼容性。本文将详细介绍 React 19 的主要新特性、改进、状态管理的变化以及如何顺利升级,帮助开发者充分利用这些新功能构建高效、响应迅速的应用。

目录

  1. 主要新特性
  2. 改进与优化
  3. 状态管理与 React Server Components
  4. 升级指南
  5. 总结

主要新特性

Actions

概述

在 React 应用中,常见的操作是执行数据变更并随后更新状态。例如,当用户提交表单更改姓名时,你需要发起 API 请求,并处理响应。在之前的版本中,开发者需要手动处理待处理状态、错误、乐观更新和顺序请求等。然而,React 19 引入了 Actions,通过简化这些流程,自动管理这些状态更新,大大提升了开发效率。

示例

升级前:手动管理待处理和错误状态

function UpdateName() {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);

const handleSubmit = async () => {
setIsPending(true);
const error = await updateName(name);
setIsPending(false);
if (error) {
setError(error);
return;
}
redirect("/path");
};

return (
<div>
<input value={name} onChange={(event) => setName(event.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}

升级后:使用 Actions 简化状态管理

function UpdateName() {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();

const handleSubmit = () => {
startTransition(async () => {
const error = await updateName(name);
if (error) {
setError(error);
return;
}
redirect("/path");
});
};

return (
<div>
<input value={name} onChange={(event) => setName(event.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}

优势

  • 待处理状态:Actions 自动管理请求的开始和结束状态,减少手动设置状态的繁琐。
  • 乐观更新:通过 useOptimistic 钩子,用户可以在请求进行时立即看到更新结果,提高响应性。
  • 错误处理:自动处理错误,支持显示错误边界并回滚乐观更新。
  • 表单支持<form> 元素支持传递函数到 actionformAction 属性,自动提交和重置表单。

新钩子(Hooks)

React 19 引入了多个新钩子,进一步简化状态管理和数据处理。

useActionState

功能:简化 Actions 的使用,返回错误、提交动作和待处理状态。

示例

const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const error = await updateName(formData.get("name"));
if (error) {
return error;
}
redirect("/path");
return null;
},
null,
);

useOptimistic

功能:支持乐观更新,在异步请求进行时立即显示最终状态。

示例

function ChangeName({ currentName, onUpdateName }) {
const [optimisticName, setOptimisticName] = useOptimistic(currentName);

const submitAction = async formData => {
const newName = formData.get("name");
setOptimisticName(newName);
const updatedName = await updateName(newName);
onUpdateName(updatedName);
};

return (
<form action={submitAction}>
<p>Your name is: {optimisticName}</p>
<p>
<label>Change Name:</label>
<input
type="text"
name="name"
disabled={currentName !== optimisticName}
/>
</p>
</form>
);
}

use

功能:在渲染时读取资源(如 Promise),并支持 Suspense。

示例

import { use } from 'react';

function Comments({ commentsPromise }) {
const comments = use(commentsPromise);
return comments.map(comment => <p key={comment.id}>{comment}</p>);
}

function Page({ commentsPromise }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}

注意use 不能用于在渲染过程中创建的 Promise,需确保 Promise 来自 Suspense 兼容的库或框架。

React DOM Static APIs

prerender 和 prerenderToNodeStream

功能:改进静态 HTML 生成,支持数据加载等待,适用于流式渲染环境。

示例

import { prerender } from 'react-dom/static';

async function handler(request) {
const { prelude } = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}

React Server Components

Server Components

功能:允许在独立于客户端应用或 SSR 服务器的环境中预渲染组件,适用于全栈 React 架构。

特点

  • 运行环境独立于客户端或 SSR 服务器。
  • 可在构建时或每个请求时执行。

Server Actions

功能:允许客户端组件调用在服务器上执行的异步函数,通过 "use server" 指令实现。

示例

// Server Component
'use server';

export async function fetchData() {
// 服务器端逻辑
}
// Client Component
import { fetchData } from './ServerComponent';

function ClientComponent() {
const handleClick = async () => {
const data = await fetchData();
// 处理数据
};

return <button onClick={handleClick}>Fetch Data</button>;
}

改进与优化

ref 作为属性

功能:函数组件现在可以直接接收 ref 作为属性,无需使用 forwardRef

示例

function MyInput({ placeholder, ref }) {
return <input placeholder={placeholder} ref={ref} />;
}

// 使用方式
<MyInput ref={inputRef} />

优势:简化函数组件中 ref 的传递,未来将逐步弃用 forwardRef

Hydration 错误的差异显示

改进:在 hydration 过程中,如果服务器和客户端渲染的内容不匹配,React 19 提供了更详细的差异信息,帮助开发者快速定位问题。

示例错误信息

Uncaught Error: Hydration failed because the server rendered HTML didn’t match the client. As a result this tree will be regenerated on the client. This can happen if an SSR-ed Client Component used:
- A server/client branch if (typeof window !== 'undefined').
- Variable input such as Date.now() or Math.random() which changes each time it’s called.
- Date formatting in a user’s locale which doesn’t match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
- ...etc.

Context 作为提供者

功能:可以直接使用 <Context> 代替 <Context.Provider>,简化上下文提供者的使用。

示例

const ThemeContext = createContext('');

function App({ children }) {
return (
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}

优势:减少代码冗余,未来将逐步弃用 <Context.Provider>

ref 回调的清理函数

功能:支持在 ref 回调中返回清理函数,确保在组件卸载时正确清理引用。

示例

<input
ref={(ref) => {
// 创建引用
// 返回清理函数
return () => {
// 清理逻辑
};
}}
/>

注意:返回清理函数后,React 将跳过调用 ref 函数时传递 null 的步骤。

useDeferredValue 初始值

功能:为 useDeferredValue 添加 initialValue 选项,允许在初始渲染时设置默认值,并在后台调度重新渲染。

示例

function Search({ deferredValue }) {
const value = useDeferredValue(deferredValue, '');

return (
<Results query={value} />
);
}

文档元数据支持

功能:原生支持在组件中渲染 <title><link><meta> 标签,自动提升到文档 <head> 部分,简化元数据管理。

示例

function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<title>{post.title}</title>
<meta name="author" content="Josh" />
<link rel="author" href="https://twitter.com/joshcstory/" />
<meta name="keywords" content={post.keywords} />
<p>Eee equals em-see-squared...</p>
</article>
);
}

样式表支持

功能:内置对样式表(外部和内联)的支持,确保样式按优先级正确加载,避免样式冲突和重复加载。

示例

function ComponentOne() {
return (
<Suspense fallback="loading...">
<link rel="stylesheet" href="foo.css" precedence="default" />
<link rel="stylesheet" href="bar.css" precedence="high" />
<article className="foo-class bar-class">
{...}
</article>
</Suspense>
);
}

function ComponentTwo() {
return (
<div>
<p>{...}</p>
<link rel="stylesheet" href="baz.css" precedence="default" /> {/* 将插入到 foo.css 和 bar.css 之间 */}
</div>
);
}

function App() {
return (
<>
<ComponentOne />
...
<ComponentOne /> {/* 不会导致重复加载样式表 */}
</>
);
}

异步脚本支持

功能:改进对异步脚本的支持,允许在组件树的任意位置渲染 <script async>,并自动去重,确保脚本只加载一次。

示例

function MyComponent() {
return (
<div>
<script async src="https://example.com/script.js" />
Hello World
</div>
);
}

function App() {
return (
<html>
<body>
<MyComponent />
...
<MyComponent /> {/* 不会导致重复加载脚本 */}
</body>
</html>
);
}

资源预加载支持

功能:新增 prefetchDNSpreconnectpreloadpreinit API,优化资源加载性能,提升页面加载速度。

示例

import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';

function MyComponent() {
preinit('https://example.com/script.js', { as: 'script' }); // 立即加载并执行脚本
preload('https://example.com/font.woff', { as: 'font' }); // 预加载字体
preload('https://example.com/stylesheet.css', { as: 'style' }); // 预加载样式表
prefetchDNS('https://example.com'); // 预取 DNS
preconnect('https://example.com'); // 预连接
}

第三方脚本和扩展的兼容性

改进:在 hydration 过程中,React 19 更好地处理第三方脚本和浏览器扩展插入的内容,避免因内容不匹配导致的错误。

更好的错误报告

改进:优化错误处理机制,减少错误日志的重复,提供更清晰的错误信息,并新增 onCaughtErroronUncaughtError 等根选项。

示例

升级前的错误日志

Uncaught Error: hit
at Throws
at renderWithHooks

Uncaught Error: hit <-- 重复
at Throws
at renderWithHooks

升级后的错误日志

Error: hit
at Throws
at renderWithHooks


The above error occurred in the Throws component:
at Throws
at ErrorBoundary
at App

自定义元素支持

功能:完全支持自定义元素,确保在客户端和 SSR 环境下属性和属性的正确处理,提高与自定义元素的兼容性。

示例

const ThemeContext = createContext('');

function App({ children }) {
return (
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}

注意:在服务器端渲染时,基于属性类型的不同,决定是将其渲染为属性还是忽略。

状态管理与 React Server Components

React 19 的 Server Components 与 Next.js 的 Server Components 有何相似与不同?

相似之处

  1. 服务器端渲染(SSR)
    • React Server ComponentsNext.js 的 Server Components 都支持在服务器端渲染组件。这意味着可以在服务器上预先渲染部分 UI,减少客户端的渲染负担,提高性能。
  2. 数据获取优化
    • 两者都允许在服务器端获取数据,并将其传递给组件进行渲染,从而优化数据加载和页面响应速度。

不同之处

  1. 集成程度

    • Next.js 是一个全功能的框架,集成了 React Server Components 以及路由、API 路由、静态生成(SSG)等多种功能,提供了一套完整的开发体验。
    • React Server Components 是 React 本身的一个特性,可以在任何使用 React 的项目中使用,而不仅限于 Next.js。它提供了更灵活的组件渲染方式,但需要开发者自行集成和配置。
  2. 使用方式

    • Next.js 的 Server Components 与其页面和 API 路由紧密结合,开发者可以通过文件系统路由轻松管理服务器组件。
    • React Server Components 在 React 19 中更加通用,适用于各种项目架构,开发者需要手动处理服务器和客户端组件的分离与通信。

React 19 中是否仍需使用第三方状态管理库(如 Zustand)?

React 19 的新特性对状态管理的影响

React 19 引入了 ActionsuseActionState 等新特性,这些特性旨在简化数据变更和状态管理的流程,自动处理待处理状态、错误处理、乐观更新等。这些内置功能确实在一定程度上简化了状态管理,减少了手动管理状态的代码量。

仍需使用第三方状态管理库的原因

尽管 React 19 提供了更强大的内置状态管理工具,但在以下情况下,第三方状态管理库如 Zustand 仍然具有不可替代的优势:

  1. 复杂的全局状态管理

    • 对于大型应用,需要管理复杂的全局状态,第三方库如 Zustand 提供了更简洁和高效的 API,能够更好地处理复杂的状态逻辑。
  2. 持久化和中间件

    • Zustand 支持状态持久化、时间旅行、状态快照等高级功能,这些功能在 React 19 的内置特性中尚未完全覆盖。
  3. 跨组件共享状态

    • 虽然 React 的 Context API 可以实现跨组件共享状态,但 Zustand 提供了更为简单和高效的方式来管理和访问共享状态,减少了样板代码。
  4. 性能优化

    • Zustand 通过选择性订阅和最小化重新渲染,能够在性能上提供额外的优化,特别是在高频更新的场景中表现更佳。

React 19 是否可以省略一些第三方库的使用?

React 19 的更新确实在某些方面可以减少对第三方库的依赖,但这取决于具体的应用需求:

  1. 简单的状态管理

    • 对于状态逻辑较为简单的小型应用,React 19 的内置特性如 Actions 和 useActionState 可以满足需求,减少对 Zustand 等库的依赖。
  2. 表单处理

    • React 19 对表单的支持更加完善,提供了自动提交和重置表单的功能,可能减少对像 Formik 这样的表单管理库的需求。
  3. 数据获取和缓存

    • 新的 use 和 Suspense 特性简化了数据获取和缓存逻辑,在某些情况下可以减少对像 React Query 这样的数据管理库的依赖。

总结

React 19 通过引入 ActionsuseActionStateReact Server Components 等新特性,确实在简化状态管理和服务器端渲染方面迈出了重要的一步。这些内置功能能够减少对某些第三方库的依赖,尤其是在处理简单状态和数据获取时。

然而,对于复杂的全局状态管理高级功能需求(如持久化、中间件)以及性能优化等方面,第三方状态管理库如 Zustand 仍然是非常有价值的工具。它们提供了 React 19 内置特性尚未覆盖的功能,能够更好地满足大型应用和复杂场景的需求。

因此,是否需要使用第三方状态管理库,应根据具体项目的需求和复杂性来决定。对于追求简洁和高效的状态管理,React 19 提供的内置工具已经能够满足很多需求,但在更复杂的场景下,第三方库仍然不可或缺。

升级指南

升级到 React 19 涉及一些关键步骤和注意事项。以下是详细的升级指南,帮助你顺利过渡到最新版本。

安装 React 19

注意:新 JSX Transform 现在是必需的

React 19 引入了新的 JSX 转换,需要确保项目中启用了新的 JSX 转换以利用 ref 作为 prop 和其他性能改进。如果未启用新的 JSX 转换,将会看到以下警告:

Your app (or one of its dependencies) is using an outdated JSX transform. Update to the modern JSX transform for faster performance: https://react.dev/link/new-jsx-transform

安装最新版本的 React 和 React DOM

使用 npm:

npm install --save-exact react@^19.0.0 react-dom@^19.0.0

使用 Yarn:

yarn add --exact react@^19.0.0 react-dom@^19.0.0

如果使用 TypeScript,还需要更新类型定义

使用 npm:

npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0

使用 Yarn:

yarn add --exact @types/react@^19.0.0 @types/react-dom@^19.0.0

使用 Codemods 进行代码迁移

为了简化升级过程,React 团队与 codemod.com 合作,发布了一系列 codemods,能够自动更新代码以适应 React 19 的新 API 和模式。

运行所有 React 19 的 codemods

使用以下命令运行 React 19 的迁移脚本:

npx codemod@latest react/19/migration-recipe

这将运行 react-codemod 仓库中的以下 codemods:

  • replace-reactdom-render
  • replace-string-ref
  • replace-act-import
  • replace-use-form-state
  • prop-types-typescript

注意:这不包括 TypeScript 的更改,详见下文。

查看所有可用的 codemods

请访问 react-codemod 仓库 获取所有可用的 codemod 列表。

处理破坏性变更

React 19 引入了一些破坏性变更,以下是主要变更及其处理方法:

渲染中的错误不再重新抛出

改进:减少错误日志的重复,错误处理方式有所变化。

升级前

在 DEV 模式下,渲染过程中抛出的错误会被捕获并重新抛出,同时记录到 console.error,导致错误日志重复。

升级后

  • 未捕获的错误:报告给 window.reportError
  • 捕获的错误:仅记录到 console.error

解决方法

如果你的生产环境错误报告依赖于错误被重新抛出,可能需要更新错误处理逻辑。你可以在创建根时添加自定义错误处理:

const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// 记录错误报告
},
onCaughtError: (error, errorInfo) => {
// 记录错误报告
}
});

移除已弃用的 React API

1. 移除函数组件的 propTypes 和 defaultProps

  • PropTypes:React 19 移除了 React 包中的 propTypes 检查,建议迁移到 TypeScript 或其他类型检查方案。

升级前

import PropTypes from 'prop-types';

function Heading({ text }) {
return <h1>{text}</h1>;
}
Heading.propTypes = {
text: PropTypes.string,
};
Heading.defaultProps = {
text: 'Hello, world!',
};

升级后

interface Props {
text?: string;
}

function Heading({ text = 'Hello, world!' }: Props) {
return <h1>{text}</h1>;
}

使用 Codemod 迁移 propTypes 到 TypeScript

npx codemod@latest react/prop-types-typescript

2. 移除旧版 Context API(contextTypes 和 getChildContext)

旧版 Context 仅在类组件中使用,React 19 移除了这些 API。

升级前

import PropTypes from 'prop-types';

class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};

getChildContext() {
return { foo: 'bar' };
}

render() {
return <Child />;
}
}

class Child extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};

render() {
return <div>{this.context.foo}</div>;
}
}

升级后

const FooContext = React.createContext();

class Parent extends React.Component {
render() {
return (
<FooContext.Provider value='bar'>
<Child />
</FooContext.Provider>
);
}
}

class Child extends React.Component {
static contextType = FooContext;

render() {
return <div>{this.context}</div>;
}
}

3. 移除字符串 refs

字符串 refs 已被弃用,React 19 移除了对字符串 refs 的支持,建议迁移到回调 refs。

升级前

class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}

render() {
return <input ref='input' />;
}
}

升级后

class MyComponent extends React.Component {
componentDidMount() {
this.input.focus();
}

render() {
return <input ref={input => this.input = input} />;
}
}

使用 Codemod 迁移字符串 refs 到回调 refs

npx codemod@latest react/19/replace-string-ref

4. 移除模块模式工厂和 React.createFactory

这些功能在现代 React 中已很少使用,React 19 移除了对它们的支持,建议使用 JSX 代替。

升级前

import { createFactory } from 'react';

const button = createFactory('button');

升级后

const button = <button />;

5. 移除 react-test-renderer/shallow

React 19 移除了 react-test-renderer/shallow,建议使用 react-shallow-renderer 或其他测试库。

升级前

import ShallowRenderer from 'react-test-renderer/shallow';

升级后

import ShallowRenderer from 'react-shallow-renderer';

6. 移除 ReactDOMTestUtils.act

act 已从 react-dom/test-utils 移动到 react 包中。

升级前

import { act } from 'react-dom/test-utils';

升级后

import { act } from 'react';

使用 Codemod 迁移 act 导入路径

npx codemod@latest react/19/replace-act-import

7. 移除 ReactDOM.render 和 ReactDOM.hydrate

React 19 移除了 ReactDOM.renderReactDOM.hydrate,建议使用 ReactDOM.createRootReactDOM.hydrateRoot

升级前

import { render, hydrate } from 'react-dom';

render(<App />, document.getElementById('root'));
hydrate(<App />, document.getElementById('root'));

升级后

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

const root = createRoot(document.getElementById('root'));
root.render(<App />);

hydrateRoot(document.getElementById('root'), <App />);

使用 Codemod 迁移 ReactDOM.render 和 ReactDOM.hydrate

npx codemod@latest react/19/replace-reactdom-render

8. 移除 ReactDOM.findDOMNode

findDOMNode 已被移除,建议使用 DOM refs 代替。

升级前

import { findDOMNode } from 'react-dom';

function AutoselectingInput() {
useEffect(() => {
const input = findDOMNode(this);
input.select()
}, []);

return <input defaultValue="Hello" />;
}

升级后

function AutoselectingInput() {
const ref = useRef(null);
useEffect(() => {
ref.current.select();
}, []);

return <input ref={ref} defaultValue="Hello" />;
}

应对新弃用的 API

React 19 引入了一些新弃用的 API,以下是主要弃用的 API 及其应对方法:

1. Deprecated: element.ref

说明:React 19 支持 ref 作为 prop,弃用了 element.ref

警告信息

Accessing element.ref is no longer supported. ref is now a regular prop. It will be removed from the JSX Element type in a future release.

解决方法:使用 props.ref 代替 element.ref

2. Deprecated: react-test-renderer

说明:React 19 弃用了 react-test-renderer,建议迁移到 @testing-library/react@testing-library/react-native

升级建议

  • 安装新的测试库:
npm install --save-dev @testing-library/react
  • 更新测试代码以使用新的测试库。

TypeScript 的更新

React 19 对 TypeScript 类型进行了多项更新,以下是主要更改及其应对方法:

1. 移除已弃用的 TypeScript 类型

React 19 清理了基于已移除 API 的 TypeScript 类型。你可能需要运行以下 codemod 来迁移类型相关的变更:

npx types-react-codemod@latest preset-19 ./path-to-app

2. ref 清理函数要求

由于引入了 ref 清理函数,TypeScript 现在要求 ref 回调不返回任何值。

解决方法:停止使用隐式返回,改为显式返回。

升级前

<div ref={current => (instance = current)} />

升级后

<div ref={current => { instance = current; }} />

运行 codemod

npx codemod@latest react-19 codemod preset-19 ./path-to-app

3. useRef 需要传递参数

useRef 现在要求传递一个初始值,简化了类型签名。

升级前

const ref = useRef<number>(null);
ref.current = 1; // TypeScript 错误

升级后

const ref = useRef<number | null>(null);
ref.current = 1;

4. ReactElement 类型的更改

ReactElementprops 现在默认为 unknown,建议显式指定类型参数。

升级前

type Example = ReactElement["props"]; // any

升级后

type Example = ReactElement<{ id: string }>["props"]; // { id: string }

运行 codemod

npx types-react-codemod@latest react-element-default-any-props ./path-to-your-react-ts-files

5. TypeScript 中的 JSX 命名空间

React 19 移除了全局的 JSX 命名空间,转而使用 React.JSX

升级前

// global.d.ts
declare namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}

升级后

// global.d.ts
declare module "react" {
namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}
}

6. 改进的 useReducer 类型推断

useReducer 现在改进了类型推断,不再接受完整的 reducer 类型作为类型参数,建议不传递类型参数,依赖上下文类型推断。

升级前

useReducer<React.Reducer<State, Action>>(reducer);

升级后

useReducer(reducer);

在边缘情况下,可以使用元组传递类型参数

useReducer<State, [Action]>(reducer);

或在函数参数上注释类型

useReducer((state: State, action: Action) => state);

状态管理与 React Server Components

React 19 的 Server Components 与 Next.js 的 Server Components 有何相似与不同?

相似之处

  1. 服务器端渲染(SSR)
    • React Server ComponentsNext.js 的 Server Components 都支持在服务器端渲染组件。这意味着可以在服务器上预先渲染部分 UI,减少客户端的渲染负担,提高性能。
  2. 数据获取优化
    • 两者都允许在服务器端获取数据,并将其传递给组件进行渲染,从而优化数据加载和页面响应速度。

不同之处

  1. 集成程度

    • Next.js 是一个全功能的框架,集成了 React Server Components 以及路由、API 路由、静态生成(SSG)等多种功能,提供了一套完整的开发体验。
    • React Server Components 是 React 本身的一个特性,可以在任何使用 React 的项目中使用,而不仅限于 Next.js。它提供了更灵活的组件渲染方式,但需要开发者自行集成和配置。
  2. 使用方式

    • Next.js 的 Server Components 与其页面和 API 路由紧密结合,开发者可以通过文件系统路由轻松管理服务器组件。
    • React Server Components 在 React 19 中更加通用,适用于各种项目架构,开发者需要手动处理服务器和客户端组件的分离与通信。

React 19 中是否仍需使用第三方状态管理库(如 Zustand)?

React 19 的新特性对状态管理的影响

React 19 引入了 ActionsuseActionState 等新特性,这些特性旨在简化数据变更和状态管理的流程,自动处理待处理状态、错误处理、乐观更新等。这些内置功能确实在一定程度上简化了状态管理,减少了手动管理状态的代码量。

仍需使用第三方状态管理库的原因

尽管 React 19 提供了更强大的内置状态管理工具,但在以下情况下,第三方状态管理库如 Zustand 仍然具有不可替代的优势:

  1. 复杂的全局状态管理

    • 对于大型应用,需要管理复杂的全局状态,第三方库如 Zustand 提供了更简洁和高效的 API,能够更好地处理复杂的状态逻辑。
  2. 持久化和中间件

    • Zustand 支持状态持久化、时间旅行、状态快照等高级功能,这些功能在 React 19 的内置特性中尚未完全覆盖。
  3. 跨组件共享状态

    • 虽然 React 的 Context API 可以实现跨组件共享状态,但 Zustand 提供了更为简单和高效的方式来管理和访问共享状态,减少了样板代码。
  4. 性能优化

    • Zustand 通过选择性订阅和最小化重新渲染,能够在性能上提供额外的优化,特别是在高频更新的场景中表现更佳。

React 19 的更新是否可以省略一些第三方库的使用?

React 19 的更新确实在某些方面可以减少对第三方库的依赖,但这取决于具体的应用需求:

  1. 简单的状态管理

    • 对于状态逻辑较为简单的小型应用,React 19 的内置特性如 Actions 和 useActionState 可以满足需求,减少对 Zustand 等库的依赖。
  2. 表单处理

    • React 19 对表单的支持更加完善,提供了自动提交和重置表单的功能,可能减少对像 Formik 这样的表单管理库的需求。
  3. 数据获取和缓存

    • 新的 use 和 Suspense 特性简化了数据获取和缓存逻辑,在某些情况下可以减少对像 React Query 这样的数据管理库的依赖。

升级指南

升级到 React 19 涉及一些关键步骤和注意事项。以下是详细的升级指南,帮助你顺利过渡到最新版本。

安装 React 19

注意:新 JSX Transform 现在是必需的

React 19 引入了新的 JSX 转换,需要确保项目中启用了新的 JSX 转换以利用 ref 作为 prop 和其他性能改进。如果未启用新的 JSX 转换,将会看到以下警告:

Your app (or one of its dependencies) is using an outdated JSX transform. Update to the modern JSX transform for faster performance: https://react.dev/link/new-jsx-transform

安装最新版本的 React 和 React DOM

使用 npm:

npm install --save-exact react@^19.0.0 react-dom@^19.0.0

使用 Yarn:

yarn add --exact react@^19.0.0 react-dom@^19.0.0

如果使用 TypeScript,还需要更新类型定义

使用 npm:

npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0

使用 Yarn:

yarn add --exact @types/react@^19.0.0 @types/react-dom@^19.0.0

使用 Codemods 进行代码迁移

为了简化升级过程,React 团队与 codemod.com 合作,发布了一系列 codemods,能够自动更新代码以适应 React 19 的新 API 和模式。

运行所有 React 19 的 codemods

使用以下命令运行 React 19 的迁移脚本:

npx codemod@latest react/19/migration-recipe

这将运行 react-codemod 仓库中的以下 codemods:

  • replace-reactdom-render
  • replace-string-ref
  • replace-act-import
  • replace-use-form-state
  • prop-types-typescript

注意:这不包括 TypeScript 的更改,详见下文。

查看所有可用的 codemods

请访问 react-codemod 仓库 获取所有可用的 codemod 列表。

处理破坏性变更

React 19 引入了一些破坏性变更,以下是主要变更及其处理方法:

渲染中的错误不再重新抛出

改进:减少错误日志的重复,错误处理方式有所变化。

升级前

在 DEV 模式下,渲染过程中抛出的错误会被捕获并重新抛出,同时记录到 console.error,导致错误日志重复。

升级后

  • 未捕获的错误:报告给 window.reportError
  • 捕获的错误:仅记录到 console.error

解决方法

如果你的生产环境错误报告依赖于错误被重新抛出,可能需要更新错误处理逻辑。你可以在创建根时添加自定义错误处理:

const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// 记录错误报告
},
onCaughtError: (error, errorInfo) => {
// 记录错误报告
}
});

移除已弃用的 React API

1. 移除函数组件的 propTypes 和 defaultProps

  • PropTypes:React 19 移除了 React 包中的 propTypes 检查,建议迁移到 TypeScript 或其他类型检查方案。

升级前

import PropTypes from 'prop-types';

function Heading({ text }) {
return <h1>{text}</h1>;
}
Heading.propTypes = {
text: PropTypes.string,
};
Heading.defaultProps = {
text: 'Hello, world!',
};

升级后

interface Props {
text?: string;
}

function Heading({ text = 'Hello, world!' }: Props) {
return <h1>{text}</h1>;
}

使用 Codemod 迁移 propTypes 到 TypeScript

npx codemod@latest react/prop-types-typescript

2. 移除旧版 Context API(contextTypes 和 getChildContext)

旧版 Context 仅在类组件中使用,React 19 移除了这些 API。

升级前

import PropTypes from 'prop-types';

class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};

getChildContext() {
return { foo: 'bar' };
}

render() {
return <Child />;
}
}

class Child extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};

render() {
return <div>{this.context.foo}</div>;
}
}

升级后

const FooContext = React.createContext();

class Parent extends React.Component {
render() {
return (
<FooContext.Provider value='bar'>
<Child />
</FooContext.Provider>
);
}
}

class Child extends React.Component {
static contextType = FooContext;

render() {
return <div>{this.context}</div>;
}
}

3. 移除字符串 refs

字符串 refs 已被弃用,React 19 移除了对字符串 refs 的支持,建议迁移到回调 refs。

升级前

class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}

render() {
return <input ref='input' />;
}
}

升级后

class MyComponent extends React.Component {
componentDidMount() {
this.input.focus();
}

render() {
return <input ref={input => this.input = input} />;
}
}

使用 Codemod 迁移字符串 refs 到回调 refs

npx codemod@latest react/19/replace-string-ref

4. 移除模块模式工厂和 React.createFactory

这些功能在现代 React 中已很少使用,React 19 移除了对它们的支持,建议使用 JSX 代替。

升级前

import { createFactory } from 'react';

const button = createFactory('button');

升级后

const button = <button />;

5. 移除 react-test-renderer/shallow

React 19 移除了 react-test-renderer/shallow,建议使用 react-shallow-renderer 或其他测试库。

升级前

import ShallowRenderer from 'react-test-renderer/shallow';

升级后

import ShallowRenderer from 'react-shallow-renderer';

6. 移除 ReactDOMTestUtils.act

act 已从 react-dom/test-utils 移动到 react 包中。

升级前

import { act } from 'react-dom/test-utils';

升级后

import { act } from 'react';

使用 Codemod 迁移 act 导入路径

npx codemod@latest react/19/replace-act-import

7. 移除 ReactDOM.render 和 ReactDOM.hydrate

React 19 移除了 ReactDOM.renderReactDOM.hydrate,建议使用 ReactDOM.createRootReactDOM.hydrateRoot

升级前

import { render, hydrate } from 'react-dom';

render(<App />, document.getElementById('root'));
hydrate(<App />, document.getElementById('root'));

升级后

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

const root = createRoot(document.getElementById('root'));
root.render(<App />);

hydrateRoot(document.getElementById('root'), <App />);

使用 Codemod 迁移 ReactDOM.render 和 ReactDOM.hydrate

npx codemod@latest react/19/replace-reactdom-render

8. 移除 ReactDOM.findDOMNode

findDOMNode 已被移除,建议使用 DOM refs 代替。

升级前

import { findDOMNode } from 'react-dom';

function AutoselectingInput() {
useEffect(() => {
const input = findDOMNode(this);
input.select()
}, []);

return <input defaultValue="Hello" />;
}

升级后

function AutoselectingInput() {
const ref = useRef(null);
useEffect(() => {
ref.current.select();
}, []);

return <input ref={ref} defaultValue="Hello" />;
}

应对新弃用的 API

React 19 引入了一些新弃用的 API,以下是主要弃用的 API 及其应对方法:

1. Deprecated: element.ref

说明:React 19 支持 ref 作为 prop,弃用了 element.ref

警告信息

Accessing element.ref is no longer supported. ref is now a regular prop. It will be removed from the JSX Element type in a future release.

解决方法:使用 props.ref 代替 element.ref

2. Deprecated: react-test-renderer

说明:React 19 弃用了 react-test-renderer,建议迁移到 @testing-library/react@testing-library/react-native

升级建议

  • 安装新的测试库:
npm install --save-dev @testing-library/react
  • 更新测试代码以使用新的测试库。

TypeScript 的更新

React 19 对 TypeScript 类型进行了多项更新,以下是主要更改及其应对方法:

1. 移除已弃用的 TypeScript 类型

React 19 清理了基于已移除 API 的 TypeScript 类型。你可能需要运行以下 codemod 来迁移类型相关的变更:

npx types-react-codemod@latest preset-19 ./path-to-app

2. ref 清理函数要求

由于引入了 ref 清理函数,TypeScript 现在要求 ref 回调不返回任何值。

解决方法:停止使用隐式返回,改为显式返回。

升级前

<div ref={current => (instance = current)} />

升级后

<div ref={current => { instance = current; }} />

运行 codemod

npx codemod@latest react-19 codemod preset-19 ./path-to-app

3. useRef 需要传递参数

useRef 现在要求传递一个初始值,简化了类型签名。

升级前

const ref = useRef<number>(null);
ref.current = 1; // TypeScript 错误

升级后

const ref = useRef<number | null>(null);
ref.current = 1;

4. ReactElement 类型的更改

ReactElementprops 现在默认为 unknown,建议显式指定类型参数。

升级前

type Example = ReactElement["props"]; // any

升级后

type Example = ReactElement<{ id: string }>["props"]; // { id: string }

运行 codemod

npx types-react-codemod@latest react-element-default-any-props ./path-to-your-react-ts-files

5. TypeScript 中的 JSX 命名空间

React 19 移除了全局的 JSX 命名空间,转而使用 React.JSX

升级前

// global.d.ts
declare namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}

升级后

// global.d.ts
declare module "react" {
namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}
}

6. 改进的 useReducer 类型推断

useReducer 现在改进了类型推断,不再接受完整的 reducer 类型作为类型参数,建议不传递类型参数,依赖上下文类型推断。

升级前

useReducer<React.Reducer<State, Action>>(reducer);

升级后

useReducer(reducer);

在边缘情况下,可以使用元组传递类型参数

useReducer<State, [Action]>(reducer);

或在函数参数上注释类型

useReducer((state: State, action: Action) => state);

状态管理与 React Server Components

React 19 的 Server Components 与 Next.js 的 Server Components 有何相似与不同?

相似之处

  1. 服务器端渲染(SSR)
    • React Server ComponentsNext.js 的 Server Components 都支持在服务器端渲染组件。这意味着可以在服务器上预先渲染部分 UI,减少客户端的渲染负担,提高性能。
  2. 数据获取优化
    • 两者都允许在服务器端获取数据,并将其传递给组件进行渲染,从而优化数据加载和页面响应速度。

不同之处

  1. 集成程度

    • Next.js 是一个全功能的框架,集成了 React Server Components 以及路由、API 路由、静态生成(SSG)等多种功能,提供了一套完整的开发体验。
    • React Server Components 是 React 本身的一个特性,可以在任何使用 React 的项目中使用,而不仅限于 Next.js。它提供了更灵活的组件渲染方式,但需要开发者自行集成和配置。
  2. 使用方式

    • Next.js 的 Server Components 与其页面和 API 路由紧密结合,开发者可以通过文件系统路由轻松管理服务器组件。
    • React Server Components 在 React 19 中更加通用,适用于各种项目架构,开发者需要手动处理服务器和客户端组件的分离与通信。

React 19 中是否仍需使用第三方状态管理库(如 Zustand)?

React 19 的新特性对状态管理的影响

React 19 引入了 ActionsuseActionState 等新特性,这些特性旨在简化数据变更和状态管理的流程,自动处理待处理状态、错误处理、乐观更新等。这些内置功能确实在一定程度上简化了状态管理,减少了手动管理状态的代码量。

仍需使用第三方状态管理库的原因

尽管 React 19 提供了更强大的内置状态管理工具,但在以下情况下,第三方状态管理库如 Zustand 仍然具有不可替代的优势:

  1. 复杂的全局状态管理

    • 对于大型应用,需要管理复杂的全局状态,第三方库如 Zustand 提供了更简洁和高效的 API,能够更好地处理复杂的状态逻辑。
  2. 持久化和中间件

    • Zustand 支持状态持久化、时间旅行、状态快照等高级功能,这些功能在 React 19 的内置特性中尚未完全覆盖。
  3. 跨组件共享状态

    • 虽然 React 的 Context API 可以实现跨组件共享状态,但 Zustand 提供了更为简单和高效的方式来管理和访问共享状态,减少了样板代码。
  4. 性能优化

    • Zustand 通过选择性订阅和最小化重新渲染,能够在性能上提供额外的优化,特别是在高频更新的场景中表现更佳。

React 19 是否可以省略一些第三方库的使用?

React 19 的更新确实在某些方面可以减少对第三方库的依赖,但这取决于具体的应用需求:

  1. 简单的状态管理

    • 对于状态逻辑较为简单的小型应用,React 19 的内置特性如 Actions 和 useActionState 可以满足需求,减少对 Zustand 等库的依赖。
  2. 表单处理

    • React 19 对表单的支持更加完善,提供了自动提交和重置表单的功能,可能减少对像 Formik 这样的表单管理库的需求。
  3. 数据获取和缓存

    • 新的 use 和 Suspense 特性简化了数据获取和缓存逻辑,在某些情况下可以减少对像 React Query 这样的数据管理库的依赖。

总结

React 19 通过引入 ActionsuseActionStateReact Server Components 等新特性,确实在简化状态管理和服务器端渲染方面迈出了重要的一步。这些内置功能能够减少对某些第三方库的依赖,尤其是在处理简单状态和数据获取时。

然而,对于复杂的全局状态管理高级功能需求(如持久化、中间件)以及性能优化等方面,第三方状态管理库如 Zustand 仍然是非常有价值的工具。它们提供了 React 19 内置特性尚未覆盖的功能,能够更好地满足大型应用和复杂场景的需求。

因此,是否需要使用第三方状态管理库,应根据具体项目的需求和复杂性来决定。对于追求简洁和高效的状态管理,React 19 提供的内置工具已经能够满足很多需求,但在更复杂的场景下,第三方库仍然不可或缺。

升级指南

升级到 React 19 涉及一些关键步骤和注意事项。以下是详细的升级指南,帮助你顺利过渡到最新版本。

安装 React 19

注意:新 JSX Transform 现在是必需的

React 19 引入了新的 JSX 转换,需要确保项目中启用了新的 JSX 转换以利用 ref 作为 prop 和其他性能改进。如果未启用新的 JSX 转换,将会看到以下警告:

Your app (or one of its dependencies) is using an outdated JSX transform. Update to the modern JSX transform for faster performance: https://react.dev/link/new-jsx-transform

安装最新版本的 React 和 React DOM

使用 npm:

npm install --save-exact react@^19.0.0 react-dom@^19.0.0

使用 Yarn:

yarn add --exact react@^19.0.0 react-dom@^19.0.0

如果使用 TypeScript,还需要更新类型定义

使用 npm:

npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0

使用 Yarn:

yarn add --exact @types/react@^19.0.0 @types/react-dom@^19.0.0

使用 Codemods 进行代码迁移

为了简化升级过程,React 团队与 codemod.com 合作,发布了一系列 codemods,能够自动更新代码以适应 React 19 的新 API 和模式。

运行所有 React 19 的 codemods

使用以下命令运行 React 19 的迁移脚本:

npx codemod@latest react/19/migration-recipe

这将运行 react-codemod 仓库中的以下 codemods:

  • replace-reactdom-render
  • replace-string-ref
  • replace-act-import
  • replace-use-form-state
  • prop-types-typescript

注意:这不包括 TypeScript 的更改,详见下文。

查看所有可用的 codemods

请访问 react-codemod 仓库 获取所有可用的 codemod 列表。

处理破坏性变更

React 19 引入了一些破坏性变更,以下是主要变更及其处理方法:

渲染中的错误不再重新抛出

改进:减少错误日志的重复,错误处理方式有所变化。

升级前

在 DEV 模式下,渲染过程中抛出的错误会被捕获并重新抛出,同时记录到 console.error,导致错误日志重复。

升级后

  • 未捕获的错误:报告给 window.reportError
  • 捕获的错误:仅记录到 console.error

解决方法

如果你的生产环境错误报告依赖于错误被重新抛出,可能需要更新错误处理逻辑。你可以在创建根时添加自定义错误处理:

const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// 记录错误报告
},
onCaughtError: (error, errorInfo) => {
// 记录错误报告
}
});

移除已弃用的 React API

1. 移除函数组件的 propTypes 和 defaultProps

  • PropTypes:React 19 移除了 React 包中的 propTypes 检查,建议迁移到 TypeScript 或其他类型检查方案。

升级前

import PropTypes from 'prop-types';

function Heading({ text }) {
return <h1>{text}</h1>;
}
Heading.propTypes = {
text: PropTypes.string,
};
Heading.defaultProps = {
text: 'Hello, world!',
};

升级后

interface Props {
text?: string;
}

function Heading({ text = 'Hello, world!' }: Props) {
return <h1>{text}</h1>;
}

使用 Codemod 迁移 propTypes 到 TypeScript

npx codemod@latest react/prop-types-typescript

2. 移除旧版 Context API(contextTypes 和 getChildContext)

旧版 Context 仅在类组件中使用,React 19 移除了这些 API。

升级前

import PropTypes from 'prop-types';

class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};

getChildContext() {
return { foo: 'bar' };
}

render() {
return <Child />;
}
}

class Child extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};

render() {
return <div>{this.context.foo}</div>;
}
}

升级后

const FooContext = React.createContext();

class Parent extends React.Component {
render() {
return (
<FooContext.Provider value='bar'>
<Child />
</FooContext.Provider>
);
}
}

class Child extends React.Component {
static contextType = FooContext;

render() {
return <div>{this.context}</div>;
}
}

3. 移除字符串 refs

字符串 refs 已被弃用,React 19 移除了对字符串 refs 的支持,建议迁移到回调 refs。

升级前

class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}

render() {
return <input ref='input' />;
}
}

升级后

class MyComponent extends React.Component {
componentDidMount() {
this.input.focus();
}

render() {
return <input ref={input => this.input = input} />;
}
}

使用 Codemod 迁移字符串 refs 到回调 refs

npx codemod@latest react/19/replace-string-ref

4. 移除模块模式工厂和 React.createFactory

这些功能在现代 React 中已很少使用,React 19 移除了对它们的支持,建议使用 JSX 代替。

升级前

import { createFactory } from 'react';

const button = createFactory('button');

升级后

const button = <button />;

5. 移除 react-test-renderer/shallow

React 19 移除了 react-test-renderer/shallow,建议使用 react-shallow-renderer 或其他测试库。

升级前

import ShallowRenderer from 'react-test-renderer/shallow';

升级后

import ShallowRenderer from 'react-shallow-renderer';

6. 移除 ReactDOMTestUtils.act

act 已从 react-dom/test-utils 移动到 react 包中。

升级前

import { act } from 'react-dom/test-utils';

升级后

import { act } from 'react';

使用 Codemod 迁移 act 导入路径

npx codemod@latest react/19/replace-act-import

7. 移除 ReactDOM.render 和 ReactDOM.hydrate

React 19 移除了 ReactDOM.renderReactDOM.hydrate,建议使用 ReactDOM.createRootReactDOM.hydrateRoot

升级前

import { render, hydrate } from 'react-dom';

render(<App />, document.getElementById('root'));
hydrate(<App />, document.getElementById('root'));

升级后

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

const root = createRoot(document.getElementById('root'));
root.render(<App />);

hydrateRoot(document.getElementById('root'), <App />);

使用 Codemod 迁移 ReactDOM.render 和 ReactDOM.hydrate

npx codemod@latest react/19/replace-reactdom-render

8. 移除 ReactDOM.findDOMNode

findDOMNode 已被移除,建议使用 DOM refs 代替。

升级前

import { findDOMNode } from 'react-dom';

function AutoselectingInput() {
useEffect(() => {
const input = findDOMNode(this);
input.select()
}, []);

return <input defaultValue="Hello" />;
}

升级后

function AutoselectingInput() {
const ref = useRef(null);
useEffect(() => {
ref.current.select();
}, []);

return <input ref={ref} defaultValue="Hello" />;
}

应对新弃用的 API

React 19 引入了一些新弃用的 API,以下是主要弃用的 API 及其应对方法:

1. Deprecated: element.ref

说明:React 19 支持 ref 作为 prop,弃用了 element.ref

警告信息

Accessing element.ref is no longer supported. ref is now a regular prop. It will be removed from the JSX Element type in a future release.

解决方法:使用 props.ref 代替 element.ref

2. Deprecated: react-test-renderer

说明:React 19 弃用了 react-test-renderer,建议迁移到 @testing-library/react@testing-library/react-native

升级建议

  • 安装新的测试库:
npm install --save-dev @testing-library/react
  • 更新测试代码以使用新的测试库。

TypeScript 的更新

React 19 对 TypeScript 类型进行了多项更新,以下是主要更改及其应对方法:

1. 移除已弃用的 TypeScript 类型

React 19 清理了基于已移除 API 的 TypeScript 类型。你可能需要运行以下 codemod 来迁移类型相关的变更:

npx types-react-codemod@latest preset-19 ./path-to-app

2. ref 清理函数要求

由于引入了 ref 清理函数,TypeScript 现在要求 ref 回调不返回任何值。

解决方法:停止使用隐式返回,改为显式返回。

升级前

<div ref={current => (instance = current)} />

升级后

<div ref={current => { instance = current; }} />

运行 codemod

npx codemod@latest react-19 codemod preset-19 ./path-to-app

3. useRef 需要传递参数

useRef 现在要求传递一个初始值,简化了类型签名。

升级前

const ref = useRef<number>(null);
ref.current = 1; // TypeScript 错误

升级后

const ref = useRef<number | null>(null);
ref.current = 1;

4. ReactElement 类型的更改

ReactElementprops 现在默认为 unknown,建议显式指定类型参数。

升级前

type Example = ReactElement["props"]; // any

升级后

type Example = ReactElement<{ id: string }>["props"]; // { id: string }

运行 codemod

npx types-react-codemod@latest react-element-default-any-props ./path-to-your-react-ts-files

5. TypeScript 中的 JSX 命名空间

React 19 移除了全局的 JSX 命名空间,转而使用 React.JSX

升级前

// global.d.ts
declare namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}

升级后

// global.d.ts
declare module "react" {
namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}
}

6. 改进的 useReducer 类型推断

useReducer 现在改进了类型推断,不再接受完整的 reducer 类型作为类型参数,建议不传递类型参数,依赖上下文类型推断。

升级前

useReducer<React.Reducer<State, Action>>(reducer);

升级后

useReducer(reducer);

在边缘情况下,可以使用元组传递类型参数

useReducer<State, [Action]>(reducer);

或在函数参数上注释类型

useReducer((state: State, action: Action) => state);

状态管理与 React Server Components

React 19 的 Server Components 与 Next.js 的 Server Components 有何相似与不同?

相似之处

  1. 服务器端渲染(SSR)
    • React Server ComponentsNext.js 的 Server Components 都支持在服务器端渲染组件。这意味着可以在服务器上预先渲染部分 UI,减少客户端的渲染负担,提高性能。
  2. 数据获取优化
    • 两者都允许在服务器端获取数据,并将其传递给组件进行渲染,从而优化数据加载和页面响应速度。

不同之处

  1. 集成程度

    • Next.js 是一个全功能的框架,集成了 React Server Components 以及路由、API 路由、静态生成(SSG)等多种功能,提供了一套完整的开发体验。
    • React Server Components 是 React 本身的一个特性,可以在任何使用 React 的项目中使用,而不仅限于 Next.js。它提供了更灵活的组件渲染方式,但需要开发者自行集成和配置。
  2. 使用方式

    • Next.js 的 Server Components 与其页面和 API 路由紧密结合,开发者可以通过文件系统路由轻松管理服务器组件。
    • React Server Components 在 React 19 中更加通用,适用于各种项目架构,开发者需要手动处理服务器和客户端组件的分离与通信。

React 19 中是否仍需使用第三方状态管理库(如 Zustand)?

React 19 的新特性对状态管理的影响

React 19 引入了 ActionsuseActionState 等新特性,这些特性旨在简化数据变更和状态管理的流程,自动处理待处理状态、错误处理、乐观更新等。这些内置功能确实在一定程度上简化了状态管理,减少了手动管理状态的代码量。

仍需使用第三方状态管理库的原因

尽管 React 19 提供了更强大的内置状态管理工具,但在以下情况下,第三方状态管理库如 Zustand 仍然具有不可替代的优势:

  1. 复杂的全局状态管理

    • 对于大型应用,需要管理复杂的全局状态,第三方库如 Zustand 提供了更简洁和高效的 API,能够更好地处理复杂的状态逻辑。
  2. 持久化和中间件

    • Zustand 支持状态持久化、时间旅行、状态快照等高级功能,这些功能在 React 19 的内置特性中尚未完全覆盖。
  3. 跨组件共享状态

    • 虽然 React 的 Context API 可以实现跨组件共享状态,但 Zustand 提供了更为简单和高效的方式来管理和访问共享状态,减少了样板代码。
  4. 性能优化

    • Zustand 通过选择性订阅和最小化重新渲染,能够在性能上提供额外的优化,特别是在高频更新的场景中表现更佳。

React 19 是否可以省略一些第三方库的使用?

React 19 的更新确实在某些方面可以减少对第三方库的依赖,但这取决于具体的应用需求:

  1. 简单的状态管理

    • 对于状态逻辑较为简单的小型应用,React 19 的内置特性如 Actions 和 useActionState 可以满足需求,减少对 Zustand 等库的依赖。
  2. 表单处理

    • React 19 对表单的支持更加完善,提供了自动提交和重置表单的功能,可能减少对像 Formik 这样的表单管理库的需求。
  3. 数据获取和缓存

    • 新的 use 和 Suspense 特性简化了数据获取和缓存逻辑,在某些情况下可以减少对像 React Query 这样的数据管理库的依赖。

总结

React 19 通过引入 ActionsuseActionStateReact Server Components 等新特性,确实在简化状态管理和服务器端渲染方面迈出了重要的一步。这些内置功能能够减少对某些第三方库的依赖,尤其是在处理简单状态和数据获取时。

然而,对于复杂的全局状态管理高级功能需求(如持久化、中间件)以及性能优化等方面,第三方状态管理库如 Zustand 仍然是非常有价值的工具。它们提供了 React 19 内置特性尚未覆盖的功能,能够更好地满足大型应用和复杂场景的需求。

因此,是否需要使用第三方状态管理库,应根据具体项目的需求和复杂性来决定。对于追求简洁和高效的状态管理,React 19 提供的内置工具已经能够满足很多需求,但在更复杂的场景下,第三方库仍然不可或缺。

升级指南

升级到 React 19 涉及一些关键步骤和注意事项。以下是详细的升级指南,帮助你顺利过渡到最新版本。

安装 React 19

注意:新 JSX Transform 现在是必需的

React 19 引入了新的 JSX 转换,需要确保项目中启用了新的 JSX 转换以利用 ref 作为 prop 和其他性能改进。如果未启用新的 JSX 转换,将会看到以下警告:

Your app (or one of its dependencies) is using an outdated JSX transform. Update to the modern JSX transform for faster performance: https://react.dev/link/new-jsx-transform

安装最新版本的 React 和 React DOM

使用 npm:

npm install --save-exact react@^19.0.0 react-dom@^19.0.0

使用 Yarn:

yarn add --exact react@^19.0.0 react-dom@^19.0.0

如果使用 TypeScript,还需要更新类型定义

使用 npm:

npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0

使用 Yarn:

yarn add --exact @types/react@^19.0.0 @types/react-dom@^19.0.0

使用 Codemods 进行代码迁移

为了简化升级过程,React 团队与 codemod.com 合作,发布了一系列 codemods,能够自动更新代码以适应 React 19 的新 API 和模式。

运行所有 React 19 的 codemods

使用以下命令运行 React 19 的迁移脚本:

npx codemod@latest react/19/migration-recipe

这将运行 react-codemod 仓库中的以下 codemods:

  • replace-reactdom-render
  • replace-string-ref
  • replace-act-import
  • replace-use-form-state
  • prop-types-typescript

注意:这不包括 TypeScript 的更改,详见下文。

查看所有可用的 codemods

请访问 react-codemod 仓库 获取所有可用的 codemod 列表。

处理破坏性变更

React 19 引入了一些破坏性变更,以下是主要变更及其处理方法:

渲染中的错误不再重新抛出

改进:减少错误日志的重复,错误处理方式有所变化。

升级前

在 DEV 模式下,渲染过程中抛出的错误会被捕获并重新抛出,同时记录到 console.error,导致错误日志重复。

升级后

  • 未捕获的错误:报告给 window.reportError
  • 捕获的错误:仅记录到 console.error

解决方法

如果你的生产环境错误报告依赖于错误被重新抛出,可能需要更新错误处理逻辑。你可以在创建根时添加自定义错误处理:

const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// 记录错误报告
},
onCaughtError: (error, errorInfo) => {
// 记录错误报告
}
});

移除已弃用的 React API

1. 移除函数组件的 propTypes 和 defaultProps

  • PropTypes:React 19 移除了 React 包中的 propTypes 检查,建议迁移到 TypeScript 或其他类型检查方案。

升级前

import PropTypes from 'prop-types';

function Heading({ text }) {
return <h1>{text}</h1>;
}
Heading.propTypes = {
text: PropTypes.string,
};
Heading.defaultProps = {
text: 'Hello, world!',
};

升级后

interface Props {
text?: string;
}

function Heading({ text = 'Hello, world!' }: Props) {
return <h1>{text}</h1>;
}

使用 Codemod 迁移 propTypes 到 TypeScript

npx codemod@latest react/prop-types-typescript

2. 移除旧版 Context API(contextTypes 和 getChildContext)

旧版 Context 仅在类组件中使用,React 19 移除了这些 API。

升级前

import PropTypes from 'prop-types';

class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};

getChildContext() {
return { foo: 'bar' };
}

render() {
return <Child />;
}
}

class Child extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};

render() {
return <div>{this.context.foo}</div>;
}
}

升级后

const FooContext = React.createContext();

class Parent extends React.Component {
render() {
return (
<FooContext.Provider value='bar'>
<Child />
</FooContext.Provider>
);
}
}

class Child extends React.Component {
static contextType = FooContext;

render() {
return <div>{this.context}</div>;
}
}

3. 移除字符串 refs

字符串 refs 已被弃用,React 19 移除了对字符串 refs 的支持,建议迁移到回调 refs。

升级前

class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}

render() {
return <input ref='input' />;
}
}

升级后

class MyComponent extends React.Component {
componentDidMount() {
this.input.focus();
}

render() {
return <input ref={input => this.input = input} />;
}
}

使用 Codemod 迁移字符串 refs 到回调 refs

npx codemod@latest react/19/replace-string-ref

4. 移除模块模式工厂和 React.createFactory

这些功能在现代 React 中已很少使用,React 19 移除了对它们的支持,建议使用 JSX 代替。

升级前

import { createFactory } from 'react';

const button = createFactory('button');

升级后

const button = <button />;

5. 移除 react-test-renderer/shallow

React 19 移除了 react-test-renderer/shallow,建议使用 react-shallow-renderer 或其他测试库。

升级前

import ShallowRenderer from 'react-test-renderer/shallow';

升级后

import ShallowRenderer from 'react-shallow-renderer';

6. 移除 ReactDOMTestUtils.act

act 已从 react-dom/test-utils 移动到 react 包中。

升级前

import { act } from 'react-dom/test-utils';

升级后

import { act } from 'react';

使用 Codemod 迁移 act 导入路径

npx codemod@latest react/19/replace-act-import

7. 移除 ReactDOM.render 和 ReactDOM.hydrate

React 19 移除了 ReactDOM.renderReactDOM.hydrate,建议使用 ReactDOM.createRootReactDOM.hydrateRoot

升级前

import { render, hydrate } from 'react-dom';

render(<App />, document.getElementById('root'));
hydrate(<App />, document.getElementById('root'));

升级后

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

const root = createRoot(document.getElementById('root'));
root.render(<App />);

hydrateRoot(document.getElementById('root'), <App />);

使用 Codemod 迁移 ReactDOM.render 和 ReactDOM.hydrate

npx codemod@latest react/19/replace-reactdom-render

8. 移除 ReactDOM.findDOMNode

findDOMNode 已被移除,建议使用 DOM refs 代替。

升级前

import { findDOMNode } from 'react-dom';

function AutoselectingInput() {
useEffect(() => {
const input = findDOMNode(this);
input.select()
}, []);

return <input defaultValue="Hello" />;
}

升级后

function AutoselectingInput() {
const ref = useRef(null);
useEffect(() => {
ref.current.select();
}, []);

return <input ref={ref} defaultValue="Hello" />;
}

应对新弃用的 API

React 19 引入了一些新弃用的 API,以下是主要弃用的 API 及其应对方法:

1. Deprecated: element.ref

说明:React 19 支持 ref 作为 prop,弃用了 element.ref

警告信息

Accessing element.ref is no longer supported. ref is now a regular prop. It will be removed from the JSX Element type in a future release.

解决方法:使用 props.ref 代替 element.ref

2. Deprecated: react-test-renderer

说明:React 19 弃用了 react-test-renderer,建议迁移到 @testing-library/react@testing-library/react-native

升级建议

  • 安装新的测试库:
npm install --save-dev @testing-library/react
  • 更新测试代码以使用新的测试库。

TypeScript 的更新

React 19 对 TypeScript 类型进行了多项更新,以下是主要更改及其应对方法:

1. 移除已弃用的 TypeScript 类型

React 19 清理了基于已移除 API 的 TypeScript 类型。你可能需要运行以下 codemod 来迁移类型相关的变更:

npx types-react-codemod@latest preset-19 ./path-to-app

2. ref 清理函数要求

由于引入了 ref 清理函数,TypeScript 现在要求 ref 回调不返回任何值。

解决方法:停止使用隐式返回,改为显式返回。

升级前

<div ref={current => (instance = current)} />

升级后

<div ref={current => { instance = current; }} />

运行 codemod

npx codemod@latest react-19 codemod preset-19 ./path-to-app

3. useRef 需要传递参数

useRef 现在要求传递一个初始值,简化了类型签名。

升级前

const ref = useRef<number>(null);
ref.current = 1; // TypeScript 错误

升级后

const ref = useRef<number | null>(null);
ref.current = 1;

4. ReactElement 类型的更改

ReactElementprops 现在默认为 unknown,建议显式指定类型参数。

升级前

type Example = ReactElement["props"]; // any

升级后

type Example = ReactElement<{ id: string }>["props"]; // { id: string }

运行 codemod

npx types-react-codemod@latest react-element-default-any-props ./path-to-your-react-ts-files

5. TypeScript 中的 JSX 命名空间

React 19 移除了全局的 JSX 命名空间,转而使用 React.JSX

升级前

// global.d.ts
declare namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}

升级后

// global.d.ts
declare module "react" {
namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}
}

6. 改进的 useReducer 类型推断

useReducer 现在改进了类型推断,不再接受完整的 reducer 类型作为类型参数,建议不传递类型参数,依赖上下文类型推断。

升级前

useReducer<React.Reducer<State, Action>>(reducer);

升级后

useReducer(reducer);

在边缘情况下,可以使用元组传递类型参数

useReducer<State, [Action]>(reducer);

或在函数参数上注释类型

useReducer((state: State, action: Action) => state);

总结

React 19 带来了诸多令人兴奋的新特性和改进,包括 Actions新钩子React Server Components 等,极大地简化了开发流程,提升了应用性能。通过优化错误处理、增强自定义元素支持以及引入资源预加载 API,React 19 为开发者提供了更强大的工具和更灵活的架构,助力构建高效、响应迅速的现代应用。

此外,React 19 在状态管理和服务器组件方面的更新,虽然在一定程度上减少了对第三方库的依赖,但对于复杂的状态管理和高级功能需求,第三方库如 Zustand 依然是不可或缺的工具。开发者应根据项目需求和复杂性选择最合适的状态管理方案。

升级到 React 19 涉及一些破坏性变更和新的最佳实践,但通过使用官方提供的 codemods 和详细的升级指南,你可以顺利过渡到最新版本,充分利用其新特性提升开发体验和应用性能。

如果你希望提升 React 应用的开发体验和性能,强烈建议尽快升级到 React 19,并深入探索其新特性。欲了解更多详细信息,请参阅官方 React 19 文档


参考资料

鱼雪

这是#[wasm_bindgen]的"Hello, world!"示例,展示如何设置项目, 将函数导出到JS,从JS调用并在Rust中调用警报功能。

1. 创建项目

cargo new hello-world --lib
cd hello-world

2. 添加依赖

cargo add wasm-bindgen

3. Cargo.toml

Cargo.toml 列出了 wasm-bindgen crate 作为一个依赖项。 值得注意的是,crate-type = ["cdylib"],这在当今主要用于 wasm 最终工件。

[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.92"
备注

[lib] 部分指定了库的类型为 cdylib,这意味着它将被编译为共享库

适合与其他语言(如 JavaScript)进行交互,特别是在 WebAssembly 的上下文中。

在 Rust 的 Cargo.toml 文件中,crate-type 可以指定以下几种类型:

  • lib: 默认类型,生成一个库文件(.rlib),用于其他 Rust 代码的依赖。
  • cdylib: 生成一个动态库,适合与其他语言(如 C、JavaScript)进行交互,通常用于 WebAssembly 项目。
  • rlib: 生成一个 Rust 静态库,供其他 Rust crate 使用。
  • dylib: 生成一个动态库,主要用于 Rust 生态内部的共享。
  • staticlib: 生成一个静态库,通常用于与 C/C++ 代码的交互。

你可以根据项目的需求选择合适的 crate-type

4. 编写src/lib.rs

src/lib.rs 文件包含了一个简单的函数,该函数使用 #[wasm_bindgen] 属性导出到 JavaScript。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}

上述代码中,greet 函数使用 #[wasm_bindgen] 属性导出到 JavaScript。

alert 函数是一个外部函数,它在 JavaScript 中实现。使用 extern "C" 来声明它, 然后可以在Rust代码中调用从JavaScript导入的函数。

5. 编译为WebAssembly

wasm-pack build --target web

上述命令将生成一个 pkg 目录,其中包含了编译好的 WebAssembly 模块和 TypeScript(JavaScript) 包装器。 使用 wasm-pack build --target web 来生成一个可以在浏览器中运行的包, 是一个完整的 npm 包,直接按照包的方式直接导入到 JavaScript 项目中。

如果是wasm-pack build,则可以构建一个可以在 Node.js 中运行的包。

wasm-pack build生成pkg目录

6. 在JavaScript中调用

index.js
// /path/to/hello-world/index.js

import { greet } from './pkg';

greet('World');

7. 创建npm包

pnpm init
# 或
npm init

会在当前目录下生成一个package.json文件,此文件是npm包的配置文件

8. 添加依赖

pnpm add webpack webpack-cli webpack-dev-server html-webpack-plugin -D
# 或
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D

上述几个包的作用是:

  1. webpack Webpack 是一个模块打包工具,主要用于将多个模块(如 JavaScript、CSS、图片等)打包成一个或多个文件。它可以处理依赖关系,优化资源,并支持各种加载器和插件,以满足不同的构建需求。
  2. webpack-cli webpack-cli 是 Webpack 的命令行接口,允许用户通过命令行执行 Webpack 的构建任务。它提供了一些命令和选项,使得用户可以更方便地配置和运行 Webpack,而不需要直接在代码中进行设置。
  3. webpack-dev-server webpack-dev-server 是一个用于开发的服务器,提供了热模块替换(HMR)功能,可以在代码更改时自动刷新浏览器。它使得开发过程更加高效,能够快速查看修改后的效果,而无需手动刷新页面。
  4. html-webpack-plugin html-webpack-plugin 是一个 Webpack 插件,用于生成 HTML 文件。它可以自动将打包后的 JavaScript 和 CSS 文件插入到生成的 HTML 文件中,从而简化了手动管理 HTML 的过程。它还支持模板引擎,可以根据需要自定义生成的 HTML。

9. 创建webpack.config.js

webpack.config.js
// /path/to/hello-world/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
plugins: [
new HtmlWebpackPlugin(),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, ".")
}),
],
mode: 'development',
experiments: {
asyncWebAssembly: true
}
};

上述代码的作用是:

  • 引入 Node.js 内置的 path 模块,方便处理文件路径。
  • 引入 HtmlWebpackPlugin,用于生成 HTML 文件。
  • 引入 webpack,虽然在这个配置中没有直接使用,但通常用于访问 Webpack 的功能。
  • 引入 WasmPackPlugin,用于处理 Rust 编写的 WebAssembly 模块
  • entry: 指定应用程序的入口文件,这里是 index.js。即这里是我们在第6步中创建的文件。
  • output: 指定输出文件的配置:
  • path: 输出目录,使用 path.resolve 来确保路径正确,输出到 dist 文件夹。
  • filename: 输出的文件名,这里是 index.js
  • plugins: 配置使用的插件:
  • HtmlWebpackPlugin: 自动生成 HTML 文件,包含打包后的 JavaScript 代码。
  • WasmPackPlugin: 配置 WasmPack 插件,crateDirectory 指定 Rust crate 的路径,这里是当前目录。
  • mode: 设置为 development,表示当前环境是开发模式,启用一些开发时的功能(如更好的错误信息)。
  • experiments: 启用 Webpack 的实验功能,这里开启了 asyncWebAssembly,允许使用异步加载 WebAssembly 模块。

10. 配置webpack服务

{
"name": "hello-world",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"serve": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.7.0",
"html-webpack-plugin": "^5.6.0",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
}
}

配置package.json文件,添加buildserve脚本,用于构建和启动服务。

11. 编译

pnpm run build
# 或
npm run build

使用pnpm构建代码

12. 运行

pnpm run server
# 或
npm run server

使用pnpm启动服务

打开浏览器,访问 http://localhost:8080,在控制台中可以看到输出 Hello, World!。 webpack默认的服务端口是8080,如果端口被占用,可以在webpack.config.js中配置。

浏览器中显示Hello, World!

整个项目代码结构

整个项目代码结构

链接

可能会遇到的问题

  1. wasm-pack build 时,可能会遇到以下错误:
wasm-pack build
[INFO]: 🎯 Checking for the Wasm target...
[INFO]: 🌀 Compiling to Wasm...
Compiling syn v2.0.72
Compiling wasm-bindgen-backend v0.2.92
Compiling wasm-bindgen-macro-support v0.2.92
Compiling wasm-bindgen-macro v0.2.92
Compiling wasm-bindgen v0.2.92
Compiling hello-world v0.1.0 (/Users/hal/tutorials/wasm-examples/hello-world)
Finished `release` profile [optimized] target(s) in 54.81s
[INFO]: ⬇️ Installing wasm-bindgen...
[INFO]: found wasm-opt at "/Users/hal/bin/binaryen/bin/wasm-opt"
[INFO]: Optimizing wasm binaries with `wasm-opt`...
dyld[46869]: Symbol not found: __ZTVN4wasm10PassRunnerE
Referenced from: <7458E7FE-3DC2-3048-B182-5DF54E45F6D4> /Users/hal/bin/binaryen/bin/wasm-opt
Expected in: <F032F917-10CC-3569-8C1E-2299E90E789F> /usr/local/lib/libbinaryen.dylib
Error: failed to execute `wasm-opt`: exited with signal: 6 (SIGABRT)
full command: "/Users/hal/bin/binaryen/bin/wasm-opt" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm" "-o" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm-opt.wasm" "-O"
To disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`.
Caused by: failed to execute `wasm-opt`: exited with signal: 6 (SIGABRT)
full command: "/Users/hal/bin/binaryen/bin/wasm-opt" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm" "-o" "/Users/hal/tutorials/wasm-examples/hello-world/pkg/hello_world_bg.wasm-opt.wasm" "-O"
To disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`
  • 解决方法1:

Cargo.toml 中添加 wasm-opt = false。 禁用 wasm-opt

[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.92"

[package.metadata.wasm-pack]
wasm-opt = false
  • 解决方法2:

安装 binaryen,并且将 binaryen 库的路径添加到动态库的环境变量中,DYLD_LIBRARY_PATH

brew install binaryen

在Mac系统上是这样,Linux系统上可能是 LD_LIBRARY_PATH。而且这里是使用Homebrew安装的,如果是其他方式安装的,可能路径不一样。

export DYLD_LIBRARY_PATH="/opt/homebrew/lib:$DYLD_LIBRARY_PATH"
鱼雪

wasm-bindgen 是一个强大的工具,它提供了多种方式来实现 Rust 和 JavaScript 之间的互操作性。 本文将总结 wasm-bindgen 的几种常见用法,并结合代码案例进行说明。

wasm-bindgen

1. 使用 JavaScript 模块中的自定义函数

你可以在 Rust 中使用 extern "C" 块来声明从 JavaScript 导入的函数,然后在 Rust 中调用它们。 这种方式非常适合需要调用现有 JavaScript 函数的场景。

示例

  • JavaScript 部分 (foo.js)
export function js_add(a, b) {
return a + b;
}
  • Rust 部分
use wasm_bindgen::prelude::*;

// Declare the JavaScript function
#[wasm_bindgen(module = "/js/foo.js")]
extern "C" {
fn js_add(a: i32, b: i32) -> i32;
}

// Define a Rust function that uses the imported JavaScript function
#[wasm_bindgen]
pub fn add_in_rust(a: i32, b: i32) -> i32 {
js_add(a, b)
}

2. 使用 JavaScript 基础功能

如果你需要使用 JavaScript 的基础功能,可以依赖 js-sys crate。 js-sys 提供了对 JavaScript 标准库的绑定,例如 MathDateArray 等。

示例

use wasm_bindgen::prelude::*;
use js_sys::Math;

#[wasm_bindgen]
pub fn random_number() -> f64 {
Math::random()
}

3. 使用 DOM 内容

如果你需要操作 DOM,可以依赖 web-sys crate。 web-sys 提供了对 Web API 的绑定,例如 documentwindowElement 等。

示例

use wasm_bindgen::prelude::*;
use web_sys::window;

#[wasm_bindgen]
pub fn set_document_title(title: &str) {
let window = window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
document.set_title(title);
}

4. 在 Rust 中导出给 JavaScript 使用

在 Rust 中,只要添加 #[wasm_bindgen] 宏并且将函数或类型声明为 pub, 它们就可以导出给 JavaScript 使用。

示例

  • Rust 中的代码:
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}

#[wasm_bindgen]
pub struct Person {
name: String,
age: u32,
}

#[wasm_bindgen]
impl Person {
#[wasm_bindgen(constructor)]
pub fn new(name: &str, age: u32) -> Person {
Person {
name: name.to_string(),
age,
}
}

pub fn greet(&self) -> String {
format!("Hello, my name is {} and I am {} years old.", self.name, self.age)
}
}
  • 在 JavaScript 中使用:
import init, { greet, Person } from './pkg/your_project_name.js';

async function run() {
await init();
console.log(greet("World")); // Should output "Hello, World!"

let person = new Person("Alice", 30);
console.log(person.greet()); // Should output "Hello, my name is Alice and I am 30 years old."
}

run();

5. 异步函数

你可以使用 wasm-bindgen-futures crate 来处理异步函数, 使得 Rust 中的异步函数可以在 JavaScript 中以 Promise 的形式使用。

示例

use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::window;

#[wasm_bindgen]
pub async fn fetch_data() -> Result<JsValue, JsValue> {
let window = window().expect("no global `window` exists");
let response = JsFuture::from(window.fetch_with_str("https://api.example.com/data")).await?;
let response: web_sys::Response = response.dyn_into().unwrap();
let json = JsFuture::from(response.json()?).await?;
Ok(json)
}

6. 错误处理

使用 Result 类型和 JsValue 来捕获和处理 JavaScript 异常。

示例

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn catch_js_error() -> Result<(), JsValue> {
let result = js_sys::Reflect::get(&JsValue::NULL, &JsValue::from_str("non_existent_property"));
match result {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}

7. 内存管理

注意 WebAssembly 和 JavaScript 之间的内存管理, 特别是当你在 Rust 中创建大型数据结构并传递给 JavaScript 时。

  1. 线性内存

    • WebAssembly 使用线性内存模型,这意味着内存是一个连续的字节数组。
    • WebAssembly 模块可以通过 memory 对象来访问和操作这段内存。

2.内存分配:

  • 当从 JavaScript 传递数据到 WebAssembly 时,通常需要在 WebAssembly 内存中分配空间。
  • 可以使用 wasm-bindgen 提供的 wasm_bindgen::memory() 函数来访问 WebAssembly 内存,并手动进行内存分配和释放。
  1. 内存释放:

    • WebAssembly 没有垃圾回收机制,因此需要手动管理内存。
    • 确保在不再需要数据时,及时释放内存以避免内存泄漏。
  2. 传递大型数据结构:

    • 在传递大型数据结构(如数组、字符串等)时,尽量避免不必要的拷贝。
    • 可以使用共享内存或其他优化技术来提高性能。

示例

  • Rust 中的代码:
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use js_sys::Uint8Array;

#[wasm_bindgen]
pub fn allocate_memory(size: usize) -> *mut u8 {
let mut buffer = Vec::with_capacity(size);
let ptr = buffer.as_mut_ptr();
std::mem::forget(buffer); // Prevent Rust from deallocating the memory
ptr
}

#[wasm_bindgen]
pub fn deallocate_memory(ptr: *mut u8, size: usize) {
unsafe {
let _ = Vec::from_raw_parts(ptr, size, size); // Reclaim the memory
}
}

#[wasm_bindgen]
pub fn process_data(ptr: *mut u8, size: usize) -> Result<JsValue, JsValue> {
let data = unsafe { Vec::from_raw_parts(ptr, size, size) };
let sum: u8 = data.iter().sum();
Ok(JsValue::from(sum))
}
  • JavaScript 中的代码:
import init, { allocate_memory, deallocate_memory, process_data } from './pkg/your_project_name.js';

async function run() {
await init();

const size = 10;
const ptr = allocate_memory(size);
const memory = new Uint8Array(wasm_bindgen.memory.buffer, ptr, size);

for (let i = 0; i < size; i++) {
memory[i] = i + 1;
}

const result = process_data(ptr, size);
console.log(result); // Should output the sum of the array

deallocate_memory(ptr, size);
}

run();

8. 宏简化

使用 #[wasm_bindgen(module = "...")]#[wasm_bindgen(inline_js = "...")] 等宏来简化模块导入和内联 JavaScript 代码。

示例

  • 内联 JavaScript 代码:
use wasm_bindgen::prelude::*;

#[wasm_bindgen(module = "/js/foo.js")]
extern "C" {
fn foo();
}

#[wasm_bindgen(start)]
pub fn main() {
foo();
}
  • foo.js 中:
export function foo() {
console.log("Hello from JavaScript!");
}

综合示例

以下是一个综合示例,展示了如何结合这些用法:

  • 在 Rust 中:
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{console, window, Element};

#[wasm_bindgen(module = "/js/foo.js")]
extern "C" {
fn js_add(a: i32, b: i32) -> i32;
}

#[wasm_bindgen]
pub fn add_in_rust(a: i32, b: i32) -> i32 {
js_add(a, b)
}

#[wasm_bindgen]
pub async fn fetch_data() -> Result<JsValue, JsValue> {
let window = window().expect("no global `window` exists");
let response = JsFuture::from(window.fetch_with_str("https://api.example.com/data")).await?;
let response: web_sys::Response = response.dyn_into().unwrap();
let json = JsFuture::from(response.json()?).await?;
Ok(json)
}

#[wasm_bindgen(start)]
pub fn main() {
console::log_1(&"Wasm module initialized".into());
}
  • foo.js 中:
export function js_add(a, b) {
return a + b;
}

通过这种方式,你可以充分利用 wasm-bindgen 的功能,构建强大且高效的 WebAssembly 应用程序。

#[wasm_bindgen(start)]宏来指定一个初始化函数,这个函数会在 WebAssembly 模块加载时自动调用。

总结

wasm-bindgen 提供了多种方式来实现 Rust 和 JavaScript 之间的互操作性。 通过这些方式,你可以轻松地在 Rust 和 JavaScript 之间传递数据、调用函数、处理异步操作等。 这些功能使得 WebAssembly 成为一个强大的工具,可以用于构建高性能的 Web 应用程序。

鱼雪