发布时间:2024年7月31日
在 2024.5.15 -16 日举行 ReactConf 2024 大会上 React 团队正式对外发布了 React 19 的第一个 beta 版本。
React 19 包含了重磅新特性,如:
use
hook ,为 react 中的异步编程场景提供了新方案,也帮助 Suspense 可以上桌吃饭了react forget
终于在 ReactConf 2024 上正式推出,被寄希望解决长期困扰 React 的性能问题回顾过去几年,自 React 16.8 以后,React 先后推出一些重要的功能:
react hook
的推出将函数式组件从食之无味的境地中解放出来,赋予其与类组件相同的地位,带来函数式编程在前端开发中的大规模落地,并影响到现在的前端生态createRoot
API,重构事件的挂载机制,解决多版本多实例下可能的事件触发问题 - react@17useTransition
提供了原生的类防抖函数,useDeferredValue
在防抖的基础上还可以叠加数据派生功能 - react@18上述有些 API 在我们日常的开发中并不常用,日常更多的还是通过基础的 hook 去实现业务逻辑,实际上在即将到来的 react@19 中,我们可以再一次升级我们的开发范式
在 React@19 中,官方新推出了一个名为 use
的 hook,一个没有任何场景语义的 hook,却能代表 React@19 的趋势 —— 重构异步编程范式。
先看一下使用方法:
// api.ts
export const sayHello = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: '影和' });
}, 2000);
});
};
// app.tsx
import { Suspense, use } from 'react';
import { sayHello } from './api';
function Message() {
const content = use(sayHello());
return <h1>{content.name}</h1>;
}
function App() {
return (
<Suspense fallback={<span>loading...</span>}>
<Message />
</Suspense>
);
}
export default App;
通过 Suspense
+ use
hook 的结合使用,我们终于可以在代码中像同步代码一样去编写异步逻辑,在轻量化的场景下完全可以摆脱对 useRequest
/ useQuery
等三方请求库的依赖
在使用 use
时有几个点需要注意:
use
的内容必须是一个 Promise,use
的结果是其 resolve 的值use
可以放在循环或者条件语句中使用use
必须和 Suspense
搭配一起使用,且使用 use
的组件必须为 Suspense
的子组件在 React 19 中,use(promise) 被设计成完全符合 Suspense 规范的 hook,我们可以将使用了 use(promise)的函数式组件看做成一个异步组件,即 —— Promise<React.FC>。
在这种视角下,Suspense 就相当于我们在 async / await 中使用的 try / catch,也就是说 Suspense 其实扮演了 ErrorBoundry 的角色,Suspense 中的 fallback 对应到 catch 的逻辑。
在 React 源码中,Suspense 的子组件被称为 primary
,在 React 的 diff 流程中遇到 Suspense 组件时,首先会尝试加载 primary
组件:
primary
组件只是一个普通组件,那么在下一次渲染时将直接渲染primary
组件是一个包含了 use
读取异步 promise 的组件use
故意产生的,那么就会将其 then 的回调函数保存下来,并回滚指针重新指向 Suspenseprimary
中的 promise 执行完成时「resolve」,会执行刚才保存的 then 方法,此时会触发 Suspense 的更新。由于此时 primary
中的 promise 已经 resolve,因此此时就可以拿到数据直接渲染 primary
组件。整个流程可以表示为:
相较于传统的的 useState / useEffect 等 hook ,use 有一个最大的特点 —— 它可以按需使用,即你的组件可以是这样的
function Message() {
const [flag, setFlag] = useState<boolean>();
let content = { name: 'default' };
if (flag) {
content = use(sayHello());
}
return (
<>
<h1>{content.name}</h1>
<button
onClick={() => {
setFlag(true);
}}
>
set flag
</button>
</>
);
}
我们可以将这种特性应用在某一个同时支持创建/编辑功能表单的页面中,用初始化的值作为表单的 defaultValues
,如果是在编辑页面就可以发出请求查询详情回显
import React, { use } from '@alipay/bigfish/react';
import { Form } from '@alipay/bigfish/antd';
import { useParams } from '@alipay/bigfish';
export default () => {
const { userId } = useParams();
const [form] = Form.useForm();
let initialValues = { name: 'default' };
if (userId) {
initialValues = use(queryUserDetail({ userId }));
}
return (
<Form form={form} initialValues={initialValues}>
<Form.Item name='name'>
<Input />
</Form.Item>
<Form.Item name='password'>
<Input.Password />
</Form.Item>
</Form>
);
};
在组件里,如果只是常规的使用 use(promise)
发出请求,该 hook 在组件的声明周期中只会执行一次,但有时候在某些场景下(如页面详情,手动触发刷新),我们需要多次触发刷新,此时我们可以借助 useState
来实现 use(promise)
的多次触发。
在使用中,我们可以将 use(promise)
视为一个 dependency
为传入的 promise 的 useEffect
,每当 promise 变动时,都会重新触发请求
function Message() {
const [promise, update] = useState<Promise<any>>(sayHello());
const content = use(promise);
return (
<>
<h1>{content.name}</h1>
<button onClick={() => update(sayHello())} style={{ marginTop: 16 }}>
Refresh
</button>
</>
);
}
在实际开发中,我们经常遇到对多个数据进行派生的场景,大多数情况下大家都会用 useMemo 去解决,因为其足够简单高效且符合直觉。实际上,在 React@18 中,官方提供了一个新的 hook —— useDeferredValue
,似乎更适合这种场景。
从 useDeferredValue
的名字以及目前大多数文章中对其的介绍来看,其最佳的用法是在解决 state 短时间内高频词刷新导致的性能问题上,比如:我们有一个大数量列表下前端过滤的功能,如果实时做 filter,可能会出现一些性能问题。
// 1-1000
const fullList = new Array(1000)
.fill(0)
.map((_, index) => Date.now() + `_${index}`);
function Message() {
const [keyword, setKeyword] = useState<string>();
const showList = fullList.filter((item) => {
if (!keyword) {
return true;
}
return item.includes(keyword);
});
return (
<>
<input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
style={{ border: '1px solid #000' }}
/>
<ul>
{showList?.map((name) => {
return <li key={name}>{name}</li>;
})}
</ul>
</>
);
}
仅仅是 1000 条数据,在实时做过滤时就可能出现性能上的瓶颈(搜索框卡顿),此时我们可以使用 useDeferredValue
优化我们的代码,将 filter 逻辑改为如下:
const showList = useDeferredValue(
fullList.filter((item) => {
if (!keyword) {
return true;
}
return item.includes(keyword);
})
);
相较于直接的 filter 逻辑,用户在输入框的层面的交互已经顺畅很多。
相较于其他文章中介绍的 useDeferredValue
用法,我们应该认识到这个 hook 的关键在于 deferred,而不是你传给它的是 originValue 还是 derivedValue。
在 React Reconciler 中会将任务分为多个优先级,当我们对一个数据使用 useDeferredValue
进行包裹时,相当于手动将其声明为了低优先级的任务,React 会在完成其他高优先级任务时再回来计算该数据,从而引起对应的视图更新,React 会自动管理触发计算的时间(requestAnimationFrame & requestHandleCallback
)。
这样处理有一个好处在于,如果用户设备性能很强悍,他不会感觉到有任何明显的差异,如果设备性能很差,那么这套调度的优势就会显现出来。
在 React@19 中,ref 的传递不再依赖于 forwardRef API,可以将 ref
通过 过props 的方式直接传递给子组件。
// old
export default React.forwardRef(function App(props, ref) {
// 组件逻辑
})
// new
export default function App({ ref, ...props }) {
// 组件逻辑
}
在 React@19 中,通过 useContext
创建的 context 可以直接通过
const MyContext = React.useContext({});
// old
export default () => {
<MyContext.Provider>
<App />
</MyContext.Provider>
}
// new
export default () => {
<MyContext>
<App />
</MyContext>
}
以上内容是我们可以直观看到的 React@19 带来的新的特性,简单来说 React@19 为我们带来异步编程的新范式。
可能大家觉得这些东西并不如 ahooks、react-query 等好用,而且这几年 React 推出的很多新的 API 也并没有特别为大家所知,但实际上这些东西已经在前端库中大量的运用,只是我们对这些东西的感知并屏蔽掉了。
除了上面提到的这些东西,还有一些如:useTransition、useActionState、useOptimistic
,这些在业务层面可能接触不多的东西就不再多写,大家可以自行了解。
在 React@19 中,其实隐藏了一个最最重磅的更新 —— 鸽了三年的 React Compiler(Forgot)。
有了 Compiler 加持,在未来我们可以抛弃所有的 memo / useMemo / useCallback / useDeferredValue,一个完全符合直觉、无需性能优化的函数式编程未来即将到来。