引言
就在昨天(2024.12.5),React 团队在 react.dev/blog 上发表了帖子 react.dev/blog/2024/1... React 19 正式进入了 stable 状态
React 团队介绍了一些新的特性和 Breaking Changes,并提供了升级指南,
新特性
本部分中的代码示例均来自或来自于修改后的官方示例
修改状态的"新姿势"
官方用例里,提供了一个接口请求的异步更新状态的 Demo:
在组件 UpdateName 中,handleSubmit 中会请求接口,同时把状态设置为 pending,如果接口请求错误则展示 error
于是这里分别使用了 3 个 useState
去创建了 3 个状态,用于表示数据、错误和加载状态(isPending)
javascript
// Before 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>
);
}
useTransition
React 19 提供了一个新的 Hooks:useTransition
使用 useTransition
可以以更少的代码实现这个结合异步操作并更新状态的能力
useTransition
调用后会返回一个数组,数组的第一个变量表示当前的 pending 状态,第二个变量 startTransition 为一个函数
调用这个函数并传入一个异步的函数(包括网络请求等耗时的异步动作),isPending 则会被自动设置为 true,在函数结束后被设置回 false
javascript
// Using pending state from 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>
);
}
看到这里,你可能会意识到:这里没有提供对异常的处理,使用者还是需要自行处理错误啊?那不是脱了xxxx...
别急,我们继续往下看
useActionState
使用 useActionState
,传入一个异步函数,hook 将返回 3 个变量,表示 error,action 和 pending 状态
官方用例如下:
dart
const [error, submitAction, isPending] = useActionState(
async (previousState, newName) => {
const error = await updateName(newName);
if (error) {
// You can return any result of the action.
// Here, we return only the error.
return error;
}
// handle success
return null;
},
null,
);
结合这两个 hooks,基本就可以告别一些类似 useRequest 或自己封装的请求 hooks 了
异步资源和懒加载的"新姿势"
use 关键字
在之前的提案中,React 官方就已经公布了 "use" 关键字,用于处理异步资源和状态
我们再次熟悉一下 React 19 正式版本中对 "use" 关键字的使用介绍
use
支持传入一个 Promise,并返回被 Promise 包裹的类型 T,可以将异步状态在组件返回内转化为同步数据进行渲染,同时组件也会因为使用了 use 而被"传染上"异步特性,在使用此组件时,需要用 <Susbense 标签包裹
javascript
import {use} from 'react';
function Comments({commentsPromise}) {
// `use` will suspend until the promise resolves.
const comments = use(commentsPromise);
return comments.map(comment => <p key={comment.id}>{comment}</p>);
}
function Page({commentsPromise}) {
// When `use` suspends in Comments,
// this Suspense boundary will be shown.
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
}
use 不仅可以用来处理异步资源,还可以用来读取 Context,参考以下示例:
javascript
import {use} from 'react';
import ThemeContext from './ThemeContext'
function Heading({children}) {
if (children == null) {
return null;
}
// This would not work with useContext
// because of the early return.
const theme = use(ThemeContext);
return (
<h1 style={{color: theme.color}}>
{children}
</h1>
);
}
不知道你有没有发现,不同于 useContext
hook,use 关键字支持在条件分支中被使用,这也弥补了很多 hooks 的使用缺陷
预加载资源
react-dom 提供了 prefetchDNS
、preconnect
、preload
、preinit
等 API,用于在某个时机预加载一步资源:
csharp
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom'
function MyComponent() {
preinit('https://.../path/to/some/script.js', {as: 'script' }) // loads and executes this script eagerly
preload('https://.../path/to/font.woff', { as: 'font' }) // preloads this font
preload('https://.../path/to/stylesheet.css', { as: 'style' }) // preloads this stylesheet
prefetchDNS('https://...') // when you may not actually request anything from this host
preconnect('https://...') // when you will request something but aren't sure what
}
Form 操作
React 19 提供了对于 <form 标签的一些支持
react-dom
包导出了一个新的 Hook useFormStatus
,用于在被 <form 标签包裹的子组件访问整个表单的状态:
javascript
import {useFormStatus} from 'react-dom';
function DesignButton() {
const {pending} = useFormStatus();
return <button type="submit" disabled={pending} />
}
// parent component
function DesignForm() {
const [, startTransition] = useTransition();
const handleAction = startTranstion(async () => {
// ..do something async
});
return <form action={handleAction}><DesignButton /></form>
}
这样对于复杂表单,不需要引入 Antd 等组件库也能很方便的处理状态了
更详细的介绍请参考 react.dev/reference/r...
其他特性
Ref
React 19 中支持将 ref 在 props 中传递,因此你不必再使用 forwardRef
了
原文中提到,官方将在未来删除 forwardRef
csharp
function MyInput({placeholder, ref}) {
return <input placeholder={placeholder} ref={ref} />
}
//...
<MyInput ref={ref} />
另外,在使用回调的方式获取 ref 时,支持通过返回值"清理副作用",其用法类似 useEffect
的返回值
javascript
<input
ref={(ref) => {
// ref created
// NEW: return a cleanup function to reset
// the ref when element is removed from DOM.
return () => {
// ref cleanup
};
}}
/>
Context
Context 可以直接作为一个标签使用了,无需再使用 <Context.Provider 的方式
javascript
const ThemeContext = createContext('');
function App({children}) {
return (
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}
实战
上面介绍了诸多理论性的东西,想要熟练使用这些特性,还需要亲身实践才行
使用 create-next-app
来新建一个 React 19 项目,在终端里输入命令:
React 官方已经逐渐废弃了 cra(create-react-app),在新的示例中都使用 create-next-app
当然你也可以选择其他脚手架和工具链,例如 vite,rsbuild 等
ruby
$ npx create-next-app@latest
脚手架会提示你进行一些项目配置,在完成所有配置后,你将得到一个 React 19 的项目目录
打开项目目录,查看项目结构
安装依赖并启动 App
ruby
$ pnpm i
$ pnpm run dev
打开 localhost:3000
,看到项目已经成功跑起来了
总结
相比 React 18 发布时,推出了 Concurrent Mode 并大改架构,React 19 更像是一次小的、对于使用体验的升级
如果你的项目已经在生产环境接入了18,那么不妨试试19
当然在迁移过程中,还是需要注意阅读官方文档,避免 Breaking Change 带来的线上问题