【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。

相关推荐
恋猫de小郭2 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端