React19 升级了哪些 Hook 和 API ?都有什么用?

3,258 阅读5分钟

大家好,我是双越老师,也是 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 的版本号。

image.png

使用 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
}

Kapture 2024-07-29 at 15.31.51.gif

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([]) ,它直接给我返回数据。

Kapture 2024-07-29 at 15.36.28.gif

而且,它返回的数据是你在传入的函数中自定义的,你可以返回任何想要的数据。例如你可以在提交表单时使用它,返回的数据可以是 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} />
}

Kapture 2024-07-29 at 15.39.54.gif

使用 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>
  )
}

执行这段代码,如果服务端反应较慢,可能会出现卡顿的情况,影响体验。

Kapture 2024-07-29 at 15.59.58.gif

当然,我们可以手动维护一个 loading 来优化体验,如下图

image.png

但如果我们要实现这样的效果:快速添加 todo ,每一条 todo 都有单独的 loading 效果。就像你在电梯里发微信消息一样,信号不好,发了好几条都在 loading —— 此时,一个 loading 就不够了。

image.png

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 的某个特性去重一下即可。

Kapture 2024-07-29 at 16.46.11.gif

使用 use 优化组件数据加载

当一个组件 ajax 请求数据时,需要整体增加 loading 效果,可以使用 useSuspense

父组件代码如下

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>
  )
}

Kapture 2024-07-29 at 17.32.17.gif

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 赛道 ~