【React 19 尝鲜】第一篇:use和useActionState

本文将介绍react 19的一些新特性和API,主打一个结合例子和踩坑记录的方式来分享。

一、use: 消费Context和Promise

1. Context

关于Context react19 带来了两点变化:

  • 可以使用use 在条件中消费 Context
  • 可以不再使用 <Context.Provider>,而是直接使用<Context>
tsx 复制代码
import { use } from 'react';
const ThemeContext = createContext('');

function App() {
 // ✅ 直接 ThemeContext 作为组件
  return (
    <ThemeContext value="dark">
      <Button showTheme={true}/>
    </ThemeContext>
  );  
}

function Button({ showTheme }) {
  if (showTheme) {
    // ✅ 可以在条件语句中使用 use
    const theme = use(ThemeContext);
    return <button className={theme}>Styled Button</button>;
  }

  return <button>Default Button</button>;
}

2. 消费Promise

消费promise,最常见的场景就是处理 GET类请求

下面以表格分页查询这个场景为例子,进行探索。

  • 进入页面,需要加载页面数据
  • 当修改分页(点击下一页),则请求新的数据

1.切换路由-请求数据

react 19不支持在渲染中直接创建promise 然后用use 进行消费,所以创建promise的工作交给了react-router,让它在初始化页面组件时传入一个promise。

Page.tsx

tsx 复制代码
import { useLoaderData, type ClientLoaderFunctionArgs } from "react-router";

export async function clientLoader() {
  const p = getVideos()
  
  return {
    videoListPromise: p
  }
}

export function Page() {
	const { videoListPromise } = useLoaderData<typeof clientLoader>()
	
	return <Suspense fallback={<div>Loading...</div>}>
		<VideoList {videoListPromise}/>
	</Suspense>
}

export async VideoList() {
	const { data: response } = use(videoListPromise)
	const videos = response?.data?.items || [];
	
	return <div>
		{videos.map((video) => (
			<VideoCard 
				key={video.id} 
				video={video} 
			/>
		))}
	<div>
}

P.S. 这里使用的是react-router v7 的 framework mode,clientLoader表示页面异步请求,react-router等clientLoader这个promise完成后 挂载组件,此时可以通过useLoaderData获取数据。

这个例子中,clientLoader并没有真的发起异步请求,而是返回一个pending的promise,这个是关键,就是为了让use(promise).

当渲染中,遇到use(videoListPromise) , 会throw 一个 promise,<Suspense>组件能捕获到从而渲染fallback。当这个promise 成功(resolved)就继续执行下面的渲染代码。

注意点:

  1. use()需要在<Suspense>内使用,否则就无法捕获到promise,就不能加载fallback.
  2. use()消费的promise如果失败(rejected),会触发<ErrorBoundary>(如果有的话),不会继续渲染下面的组件。在这种范式下,trycatch就没必要了,只需要定义好<ErrorBoundary>
  3. 不建议在渲染中创建promise给use使用,因为这样每次重新渲染就会重新创建一个新promise,重复请求。最佳实践是这个promise要从组件外部传入。

2.交互-请求数据

前面展示了一个"纯粹的"切换到某个页面,页面加载初始数据。但有时候需要在交互后,继续触发数据请求,比如"分页"。

此时我们会发出一个疑问 ------ promise在组件外面创建的,我的参数怎么传递呢? 答:将参数"URL 化".

先看效果:

要在 React 19 中结合 React Router 使用 use + Suspense 并利用 URL 实现分页,核心思路是:将 URL 中的查询参数(如 ?page=2)作为触发 clientLoader 重新运行的依赖项。

当 URL 改变时,React Router 会重新调用 clientLoader,产生一个新的 Promise,VideoList 组件通过 use(promise) 挂起并触发 Suspense 状态。

P.S这是 clientLoader(以及服务端的 loader)设计的核心优势之一:数据加载与路由变化天然绑定 ,无需手动监听 useParams()useLocation()。 如果你不希望 参数变化时重新加载(极少见),可以通过缓存或自定义逻辑在 clientLoader 内部跳过请求,但通常不需要。

以下是完整的改进方案:

a. 改进 clientLoader

我们需要从 request.url 中提取分页参数,并将其传递给 API 函数。

tsx 复制代码
import { useLoaderData, type ClientLoaderFunctionArgs, useSearchParams } from "react-router";
import { use, Suspense } from "react";

// 模拟 API
declare function getVideos(params: { page: number }): Promise<any>;

export async function clientLoader({ request }: ClientLoaderFunctionArgs) {
  const url = new URL(request.url);
  const page = Number(url.searchParams.get("page")) || 1; // 默认第一页
  
  // 返回一个新的 Promise
  const p = getVideos({ page });
  
  return {
    videoListPromise: p,
    currentPage: page
  };
}
b. 改进 PageVideoList 组件
tsx 复制代码
export function Page() {
  // 1. 获取 loader 传递的 Promise 和当前页码
  const { videoListPromise, currentPage } = useLoaderData<typeof clientLoader>();
  const [searchParams, setSearchParams] = useSearchParams();

  // 处理翻页逻辑:直接更新 URL 查询参数
  const handlePageChange = (newPage: number) => {
    setSearchParams((prev) => {
      prev.set("page", String(newPage));
      return prev;
    });
  };

  return (
    <div className="container">
      <h1>Video Library - Page {currentPage}</h1>
      
      {/* 2. Suspense 包裹异步渲染区域 */}
      <Suspense fallback={<div className="loading">Loading videos...</div>}>
        <VideoList videoListPromise={videoListPromise} />
      </Suspense>

      {/* 3. 分页控制器 */}
      <div className="pagination-controls">
        <button 
          disabled={currentPage <= 1} 
          onClick={() => handlePageChange(currentPage - 1)}
        >
          Previous
        </button>
        <span> Page {currentPage} </span>
        <button 
          onClick={() => handlePageChange(currentPage + 1)}
        >
          Next
        </button>
      </div>
    </div>
  );
}

// 4. VideoList 使用 React 19 的 use() 钩子
interface VideoListProps {
  videoListPromise: Promise<any>;
}

export function VideoList({ videoListPromise }: VideoListProps) {
  // use() 会自动解构 Promise,并在 Pending 时挂起 Suspense
  const response = use(videoListPromise);
  const videos = response?.data?.items || [];

  
  return (
    <div>
      {videos.map((video: any) => (
        <VideoCard key={video.id} video={video} />
      ))}
    </div>
  );
}
c. 重新加载数据的坑

第一次进入页面,触发了<Suspense>的fallback。 切换分页,重新请求数据时,useLoaderData会返回一个新的Promise {<pending>} 但是却没有触发<Suspense>的fallback。。。 有点反直觉

解决办法就是,给<Suspense>加key(不加的话,react重新渲染可能直接跳过Suspense的协调,跳过渲染) <Suspense key={currentPage} fallback={<Loading />} >

3.小总结

有些场景不太适合 clientLoader + use + <Suspense> 这种实践方式,比如满足「参数控制刷新」+ 「局部刷新」的场景。

  • 这种场景,往往是有多个请求,每个请求都需要传递参数(URL只有一个就不太好表达)。每个局部刷新对应的UI 都需要 fallback。(fallback会暂时覆盖原来的UI,不好)
  • 举个例子:「列表加载更多」的场景,往上滚动时需要加载更多数据,总不能加载时用fallback来访覆盖UI吧。还是得退化到useEffectuseEffect处理副作用终归还是必不可少的。

use(promise) 更适合"声明式、一次性解析",加载页面级数据。

🆚 对比总结

维度 use(promise) + Suspense useEffect / React Query
适用场景 页面级数据(如文章详情、商品页) 列表、表格、仪表盘等交互密集型 UI
Loading 体验 全局 fallback(看不到数据) 局部 loading(还可以看到历史数据)✅
状态控制 仅 pending/success/error 支持 isFetching, isRefetching, error, cache 等 ✅
用户交互响应 弱(被动挂起) 强(主动触发 + 精确反馈)✅
开发心智模型 声明式(数据即依赖) 命令式(事件 → 请求 → 更新)✅(对管理端更直观)
生态支持 需手动管理 React Query/SWR 提供开箱即用分页支持 ✅

二、Actions:处理表单和POST类请求

1. 什么是Actions?

1.在 React 19 中,Actions 本质上是支持异步函数的 Transition。

2.在 React 19 之前,处理异步操作通常需要手动管理 pending(加载中)、error(错误)和 data(数据)状态。Actions 的出现让这些流程自动化了。

3.当你将一个异步函数传递给 useTransitionuseActionState 时,React 会自动处理:

  • Pending 状态:自动开始并结束,无需手动 setIsLoading(true/false)。
  • 自动排队:React 保证多个提交按顺序处理。
  • UI 响应性:UI 在数据请求期间保持响应,不会阻塞。

2. react 19 的表单提交

1. 原生的表单

html 复制代码
<form action="/submit-data" method="POST">

通过onsubmit监听 submit事件,函数return true,走原生表单提交逻辑(即发送到action的url)。return false,则阻止提交

html 复制代码
<form onsubmit="return validateForm()">
    <!-- 表单字段 -->
    <input type="submit" value="提交">
</form>
<script>
    function validateForm() {
        // 验证逻辑...
        return true; // 或 false
    }
</script>

更多情况下,利用 onsubmit来手动发送http请求,提交POST请求。然后自己e.preventDefault();(阻止默认的表单提交行为)

2. 使用useActionState处理表单

React 19 对表单处理引入了较大的改进,目标是简化数据变更、状态管理和提升用户体验。

这些变化的核心是引入了「表单对action的支持」和「3个新的 Hooks」,它们旨在让您能够更多地利用 Web 标准的 <form> 元素,减少手动处理状态和 event.preventDefault() 的样板代码。

  • form元素支持action传入一个异步函数(asyncFunction
  • 提供useActionState 创建 asyncFunction 和管理异步状态
  • 提供useFormStatus 方便表单子组件 获取表单的状态
  • 提供useOptimistic 乐观更新

下面以登录表单为场景来介绍 react19 表单提交的变化。

先介绍下react19之前的版本,最直接的登录表单是如何做的。

jsx 复制代码
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Form, useNavigate } from "react-router"
import { loginApiAuthLoginPost } from "@/APIs"
import { useAuthStore } from "@/store/useAuthStore"

export default function Login() {
  const navigate = useNavigate();
  const setAuth = useAuthStore((state) => state.setAuth);
  
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    setError(null);
    setLoading(true);

    try {
      const { data, error: apiError } = await loginApiAuthLoginPost({
        body: { username, password }
      });

      if (apiError) {
        setError("Login failed. Please check your credentials.");
        return;
      }

      if (data && data.code === 200 && data.data) {
        setAuth(data.data.token, data.data.user);
        navigate("/dashboard");
      } else {
        setError(data?.err_msg || "Unknown error occurred");
      }
    } catch (err) {
      console.error(err);
      setError("An unexpected error occurred. Please try again.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleLogin} className="grid gap-4">
		<div className="grid gap-2">
		  <Label htmlFor="username">Username</Label>
		  <Input 
			id="username" 
			type="text" 
			placeholder="admin" 
			required 
			value={username}
			onChange={(e) => setUsername(e.target.value)}
		  />
		</div>
		<div className="grid gap-2">
		  <Label htmlFor="password">Password</Label>
		  <Input 
			id="password" 
			type="password" 
			required 
			value={password}
			onChange={(e) => setPassword(e.target.value)}
		  />
		</div>
		{error && (
		  <div className="text-sm text-red-500 font-medium">
			{error}
		  </div>
		)}
		<Button className="w-full" type="submit" disabled={loading}>
		  {loading ? "Signing in..." : "Sign in"}
		</Button>
	  </form>
  )
}
a. form的action属性

1.<form action> 支持函数: 现在,您可以直接将一个 异步函数(Action) 传递给 <form> 元素的 action 属性。

  • 当表单提交时,React 会自动调用这个函数,并且在函数内不需要手动调用 event.preventDefault()
  • Actions 默认在 Transition 中运行,可以自动处理 Pending (等待中) 状态、错误和乐观更新。
  • 自动重置表单。当 <form> Action 成功执行后,React 会自动为 非受控组件 重置表单。

2.web标准的FormData接口 , 原生表单的提交事件会传递一个FormData对象给函数,该对象包含了表单的键值对。

3.核心逻辑变成了 (formData: FormData) => Promise<any>的函数

jsx 复制代码
const handleLogin = async (formData: FormData) => {
    setError(null);
    setLoading(true);
    const payload = Object.fromEntries(formData.entries());
    try {
      const { data, error: apiError } = await loginApiAuthLoginPost({
        body: { ...payload }
      });
      //...
    } catch (err) {
      console.error(err);
      setError("An unexpected error occurred. Please try again.");
    } finally {
      setLoading(false);
    }

4.将handleLogin传递给form的action, 记得给form表单控件加上name="xxx"属性 。

jsx 复制代码
<form action={handleLogin} className="grid gap-4">
	<div className="grid gap-2">
	  <Label htmlFor="username">Username</Label>
	  <Input 
		id="username" 
		name="username"
		type="text" 
		placeholder="admin" 
		required 
	  />
	</div>
	<div className="grid gap-2">
	  <Label htmlFor="password">Password</Label>
	  <Input 
		id="password" 
		name="password"
		type="password" 
		required 
	  />
	</div>
	{error && (
	  <div className="text-sm text-red-500 font-medium">
		{error}
	  </div>
	)}
	<Button className="w-full" type="submit" disabled={loading}>
	  {loading ? "Signing in..." : "Sign in"}
	</Button>
  </form>
b. useActionState

useActionState介绍

  • 它将表单提交的异步操作、状态管理和加载指示结合在一起。
  • 它返回最新的状态、一个Action 函数,以及一个 isPending 状态,您可以使用它来处理表单的提交结果(如成功消息或错误)。
  • 优点: 减少了对多个 useState 的需求,简化了状态逻辑。

参数细节

  • useActionState 接受一个函数,有两个参数:

    • prevState 代表之前状态
    • formData 代表action提交的数据(如果是form表单,则传入的是FormData)
  • useActionState 可以指定两个泛型,分别对应prevState和formData的类型,比如

    • useActionState<string | null, FormData>()
  • 返回值[state, submitAction, isPending]

常见问题 有点麻烦的是:FormData对ts很不友好,你无法得知FormData上有什么属性和value。

const payload = Object.fromEntries(formData.entries()) 中的payload ts类型如下:

ts 复制代码
const payload: {  
[k: string]: FormDataEntryValue;  
}

这就要借助牛逼的zodzod-form-data两个库了,顺便把表单的前端校验也做了~

登录表单案例 使用 Actions( useActionState) 改进后的代码如下:

tsx 复制代码
import { useActionState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useNavigate } from "react-router"
import { loginApiAuthLoginPost } from "@/APIs"
import { useAuthStore } from "@/store/useAuthStore"


import { z } from "zod";
import { zfd } from "zod-form-data";

const schema = zfd.formData({
  username: zfd.text(),
  password: zfd.text(z.string().min(6))
});

export default function Login() {
  const navigate = useNavigate();
  const setAuth = useAuthStore((state) => state.setAuth);
 

  const [error, handleLogin, loading] = useActionState<string | null, FormData>(async (_prevState, formData) => {
    // const payload = Object.fromEntries(formData.entries()) //禁止这种ts不友好的转换
    const payload = schema.parse(formData)

    try {
      const { data, error: apiError } = await loginApiAuthLoginPost({
        body: payload,
      });

      if (apiError) {
        return "Login failed. Please check your credentials."
      } else if (data && data.code >= 300) {
        return data?.err_msg || "Unknown error occurred"
      }
      setAuth(data.data.token, data.data.user);
      navigate("/dashboard");
      return null;
    } catch (err) {
      return "An unexpected error occurred. Please try again."
    } 
  }, null);


  return (
    <form action={handleLogin} className="grid gap-4">
		<div className="grid gap-2">
		  <Label htmlFor="username">Username</Label>
		  <Input 
			id="username" 
			name="username"
			type="text" 
			placeholder="admin" 
			required 
		  />
		</div>
		<div className="grid gap-2">
		  <Label htmlFor="password">Password</Label>
		  <Input 
			id="password" 
			name="password"
			type="password" 
			required 
		  />
		</div>
		{error && (
		  <div className="text-sm text-red-500 font-medium">
			{error}
		  </div>
		)}
		<Button className="w-full" type="submit" disabled={loading}>
		  {loading ? "Signing in..." : "Sign in"}
		</Button>
	  </form>
  )
}
c. useFormStatus

用于在子组件中访问父级表单的状态(特别是 pending 状态),解决"属性透传(Prop Drilling)"问题。

tsx 复制代码
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "提交中..." : "提交"}</button>;
}

// 在父组件中
<form action={action}>
  <SubmitButton />
</form>
d. useOptimistic

在异步请求完成之前,先假设操作成功并立即更新 UI。如果请求失败,React 会自动回滚到旧状态。

适用的场景

  • 社交互动,比如:点赞、收藏、关注
  • 简单的列表操作,比如:删除、添加、排序
  • 单字段的表单更新,比如:修改昵称、切换某个开关(Switch)

这些场景的共同点是:逻辑简单、操作频繁、用户对即时反馈要求高、且失败率通常较低。

3. 小总结

总的来说,对于一些简单的表单 场景, 可以尽量不依赖第三方组件,做到优雅快速开发。同时Actions这种范式也不仅仅只用于form,任何POST请求都可以用这种方式来处理,减少了模板代码。 类似一个简易的useAsync hook。

相关推荐
毕设源码-邱学长15 小时前
【开题答辩全过程】以 基于VUE的打车系统的设计与实现为例,包含答辩的问题和答案
前端·javascript·vue.js
用户390513321928815 小时前
JS判断空值只知道“||”?不如来试试这个操作符
前端·javascript
海云前端115 小时前
前端面试必问 asyncawait 到底要不要加 trycatch 90% 人踩坑 求职加分技巧揭秘
前端
wuk99816 小时前
梁非线性动力学方程MATLAB编程实现
前端·javascript·matlab
XiaoYu200216 小时前
第11章 LangChain
前端·javascript·langchain
霉运全滚蛋好运围着转16 小时前
启动 Taro 4 项目报错:Error: The specified module could not be found.
前端
cxxcode16 小时前
前端模块化发展
前端
不务正业的前端学徒16 小时前
docker+nginx部署
前端
不务正业的前端学徒16 小时前
webpack/vite配置
前端