大家好,我是双越老师,也是 wangEditor 作者
我正在开发 划水AI 项目,一个 React + Node 全栈 AIGC 知识库平台,AI 写作,AI 文本处理,富文本编辑器,多人协同编辑... 欢迎围观~
前言
2024 年 4 月底,React 发布了 19 RC 版本。React 18 发布两年以后,React 即将迎来新的大版本更新。
现代 Web 前端开发框架,无论 Vue React ,在功能层面其实早就完备了,几年前的 React Hooks 和后续的 Vue Composition API 就是最后的功能改进,其后就没有了。
现在框架的升级主要是在三个方面:工具,效率,体验。
Vue 作者早就开始搞 Vite 工具生态,现在已经成为一个前端的重要工具,可见当年 Vue 作者的技术眼光。
React 18 搞 Concurrency 提高渲染效率,并且应用于很多 Server Component 功能中,也是独辟蹊径。
React 19 升级的内容非常多,其中很大一部分是关于开发和用户体验的,本文着重分析这部分。
安装
推荐使用 Next.js 进行安装,简单方便,而且 Next.js 还方便开发服务端 API ,便于测试。
npx create-next-app@latest
npm i next@rc react@rc react-dom@rc --force
安装以后,可以看到 package.json 中 react 和 react-dom 的版本号。
使用 useTrasition 简化 loading
当我们发起一个 ajax 请求时,会这样写,自己处理 data err loading 等状态。
function List() {
const [list, setList] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
async function fetchList() {
try {
const res = await fetch('/api/todo')
const data = await res.json()
setList(data.data)
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
useEffect(() => { fetchList() }, [])
// return some JSX segment
}
React18 开始给了我们一个新的选择,使用 useTransition,同样的执行效果,我们不用在维护 laoding
function List() {
const [list, setList] = useState([])
const [error, setError] = useState(null)
const [isPending, startTransition] = useTransition()
async function fetchList() {
try {
const res = await fetch('/api/todo')
const data = await res.json()
setList(data.data)
} catch (err: any) {
setError(err.message)
}
}
useEffect(() => { startTransition(fetchList) }, [])
// return some JSX segment
}
useTrasition 是 React18 发布的 Hook ,它的价值不仅仅在于维护 isPending ,还可以把一些大量计算、可能卡顿的任务放在 startTransition 中避免卡顿。
使用 useActionState 简化 ajax 代码
useActionState 是 React 19 最新的 Hooks,它继续简化代码
function List() {
const [error, setError] = useState(null)
const [list, getList, isPending] = useActionState(
async (prevList: any) => {
try {
const res = await fetch('/api/todo')
const data = await res.json()
const list = data.data
return [...prevList, ...list] // 将赋值给 list 变量
} catch (err: any) {
setError(err.message)
}
},
// 第二个参数,初始值
[
{
id: 0,
title: 'Todo 0',
completed: false,
},
]
)
useEffect(() => { getList() }, [])
// return some JSX segment
}
对比之前的代码,我不用再定义 const [list, setList] = useState([]) ,它直接给我返回数据。
而且,它返回的数据是你在传入的函数中自定义的,你可以返回任何想要的数据。例如你可以在提交表单时使用它,返回的数据可以是 error 。
function TodoInput() {
const [submitError, submitAction, isSubmitPending] = useActionState(
async (previousState: any, formData: FormData) => {
console.log('previousState ', previousState) // null ,即 useActionState 第二个参数
const todoName = formData.get('name') // 可以直接获取 form 数据
try {
const res = await fetch('/api/todo', {
method: 'POST',
body: JSON.stringify({ title: todoName }),
headers: { 'Content-Type': 'application/json' },
})
await res.json()
return null
} catch (err: any) {
return err.message
}
},
null // 定义 previousState
)
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isSubmitPending}> Add </button>
{submitError && <p>{submitError}</p>}
</form>
)
}
使用 useFormStatus 优化 submitButton
以上代码中 form 中的 submit button 需要依赖于 isSubmitPending 来判断状态。
<button type="submit" disabled={isSubmitPending}> Add </button>
还有一种方式更加简单,使用 useFormStatus ,可直接无脑获取 submit button 的 pending 状态,参考文档 react.dev/blog/2024/0…
需要注意的是,使用 useFormStatus 需要给 button 定义一个单独的 React 组件,不能和 form 混在一起。
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const {pending} = useFormStatus();
return <button type="submit" disabled={pending} />
}
使用 useOptimistic 优化多 loading 体验
如果要实现一个普通的 TodoList 功能,并使用 form 新增 todo 上传到服务端,代码如下
export default function TodoLust() {
const [list, setList] = useState([
{ id: 1, title: 'first' },
{ id: 2, title: 'second' },
])
async function submitAction(formData: FormData) {
const todoName = formData.get('name')?.toString()
await sendNewTodo(todoName || '')
}
async function sendNewTodo(title: string) {
if (!title) return
const response = await fetch('/api/todo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
})
const data = await response.json()
setList([...list, data.data])
}
return (
<div>
<h3 className="font-bold text-lg">Todo list</h3>
<form action={submitAction}>
<input type="text" name="name" autocomplete="off" />
<button type="submit">Add</button>
</form>
<ul>
{list && list.map((item: any) => <li key={item.id}>{item.title}</li>)}
</ul>
</div>
)
}
执行这段代码,如果服务端反应较慢,可能会出现卡顿的情况,影响体验。
当然,我们可以手动维护一个 loading 来优化体验,如下图
但如果我们要实现这样的效果:快速添加 todo ,每一条 todo 都有单独的 loading 效果。就像你在电梯里发微信消息一样,信号不好,发了好几条都在 loading —— 此时,一个 loading 就不够了。
React19 useOptimistic 就可以很好的解决这个问题,代码如下
export default function Home() {
// 真实 list
const [list, setList] = useState([
{ id: 1, title: 'Todo 1' },
{ id: 2, title: 'Todo 2' },
])
// 使用 useOptimistic 返回 all list - 包含 真实 list + 正在提交的 todo
const [allList, addOptTodo] = useOptimistic(list, (list, optTodo) => [...list, optTodo])
async function submitAction(formData: FormData) {
const todoName = formData.get('name')?.toString() || ''
const newTodo = {
id: Math.trunc(Math.random() * 1000),
title: todoName,
}
// 正在提交的 todo
addOptTodo({
...newTodo,
opt: true, // 标记,这是 optimistic
})
await sendNewTodo(newTodo)
}
async function sendNewTodo(newTodo: any) {
const response = await fetch('/api/todo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
const data = await response.json()
setList((l) => [...l, data.data])
// setList([...list, data.data]) //【注意】这样会遇到闭包陷阱!!!
}
return (
<div>
<h3 className="font-bold text-lg">Todo list</h3>
<form action={submitAction}>
<input type="text" name="name" autoComplete="off" />
<button type="submit">Add</button>
</form>
<ul>
{allList.map((item: any) => (
<li key={item.id}>{item.title} {item.opt && ' - loading'}</li>
))}
</ul>
</div>
)
}
从效果图来看,可能还有一点瑕疵,在渲染的时候优化一下即可,例如:根据 todo 的某个特性去重一下即可。
使用 use 优化组件数据加载
当一个组件 ajax 请求数据时,需要整体增加 loading 效果,可以使用 use 和 Suspense
父组件代码如下
async function fetchList() {
const response = await fetch('http://localhost:3000/api/todo')
const resData = (await response.json()) as any
return resData.data || []
}
export default function Home() {
const p = fetchList() // 在这里先执行函数
return (
<>
<h2>Todo List</h2>
<Suspense fallback={<div>Loading...</div>}>
<Todo fetchListPromise={p} />
</Suspense>
</>
)
}
子组件代码如下
import { use } from 'react'
export default function Todo({ fetchListPromise }) {
const list = use(fetchListPromise)
return (
<div>
<ul>
{list.map((item: any) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
)
}
在 use 之前,React 可使用 Suspense 的就是 lazy 异步组件 react.dev/reference/r…
现在 use 又补充了组件的异步数据,继续完善用户体验。PS. 你自己实现 ajax 或使用 ahooks useRequest 是无法实现 Suspense 效果的。
另外,还可以通过 use 获取 Context 信息,比较简单了。具体参考文档 react.dev/reference/r…
总结
以上就是 React19 RC 功能方面的升级,几个 Hook 和 use API,其他更多内容可参考文档 react.dev/blog/2024/0…
最后,想学习和参与 React 项目开发,可关注我的 划水AI,真实上线,有难度有复杂度,拥抱 AI 赛道 ~