请思考一个问题:如果有一个HTML
标签,React
围绕他专门出了2个hook
,那这个标签对React
未来的发展一定非常重要,这没毛病吧?
这个标签就是 —— form
。
React
围绕form
新出了如下2个hook
:
useOptimistic
useFormStatus
本文会聊聊React
围绕form
的布局与发展。
说到React
未来的发展,必须从Next.js
聊起。毕竟,React
团队成员不是加入Next
团队,就是在加入的路上。
web
开发中涉及到前后端交互的部分主要包括:
Next.js
的发展主要围绕以上两点展开。
前期,Next.js
的主打特性是SSR
、SSG
。也就是把「根据后端数据渲染前端页面」的过程从前端挪到后端。
这个时期的Next.js
路由被称为Pages Router
。
时间来到Next.js v13
,以RSC
(React Server Component)为核心的App Router
取代Pages Router
成为默认配置。
很多朋友不熟悉RSC
,认为他是实验特性。实际上,RSC
借由Next.js
已经落地了。
一句话理解RSC
—— 客户端组件(在浏览器渲染的React
组件)可以根据依赖分为两部分:
RSC
(服务端组件)state
、props
、context
)的组件,可以作为客户端组件从「根据后端数据渲染前端页面」角度看:
SSR
、SSG
是页面级别的(服务端渲染呈现的是整个页面)RSC
是组件级别的(服务端组件请求数据源)聊完了「根据后端数据渲染前端页面」,那么,围绕「根据前端用户输入保存数据到后端」,Next.js
能做哪些优化?
这就要提到Next.js
的实验特性 —— Server Action
。
「根据前端用户输入保存数据到后端」的常见场景是「表单提交」,通常我们会在form
的onSubmit
事件中做后续处理:
function Form() {
function submit() {
// ...处理formData的逻辑
// ...发送请求的逻辑
}
return (
<form onSubmit={submit}>
<input type="text"/>
<input type="text"/>
</form>
)
}
以上代码有什么可改进的地方呢?
从用户体验的角度看,如果前端禁用了JS
,那么React
不能运行,上述交互失效。如果在禁用JS
的情况下也能提交表单就好了。
从开发体验的角度看,submit
方法会发起请求,后端再根据请求携带的formData
操作数据库,比较繁琐。如果在submit
方法内能直接操作数据库就好了。
Server Action
特性就是为了实现以上2个目标。
首先来看第一个目标。
HTML
原生的form
元素有个action
属性,可以接收一个url
。当提交表单(比如点击type
为submit
的按钮)后formData
会提交给该url
。
<form action="/action_page.php" method="get">
<label for="fname">First name:</label>
<input type="text" id="fname" name="fname"><br><br>
<label for="lname">Last name:</label>
<input type="text" id="lname" name="lname"><br><br>
<input type="submit" value="Submit">
</form>
由于「提交表单」的行为是HTML
原生支持的,所以在禁用JS
的情况下也能执行。
这就是禁用JS
也能提交表单的理论基础。
React
扩展了form
的action
属性,让他除了支持url
,还能支持回调函数,比如:
function App() {
function submit(data) {
// ...
}
return (
<form action={submit}>
<! -- 省略 -->
</form>
);
如果这个回调函数内是前端执行的逻辑,则被称为client action
,比如下面这样:
async function submit(data) {
await const res = saveData(data);
// ...
}
如果这个函数内是后端执行的逻辑,则被称为server action
,比如下面这样:
"use server"
async function submit(data) {
const userID = cookies().get("userID")?.value;
await db.users.update(userID, data);
// ...
}
"use server"
标记代表这是个server action
。
如果是server action
,那么发起的请求类型是multipart/form-data
(即表单提交):
响应类型则是RSC协议
:
也就是说,有了server action
,开发者可以直接在form
的action
属性(或者button
的formAction
属性等其他几种属性)内书写后端逻辑,并且在浏览器禁用JS
的情况下这些逻辑也能执行。
为了更好的服务server action
,React
团队新出了2个hook
用于提高form
场景下的用户体验:
useOptimistic
useFormStatus
当前,这2个hook
的介绍只能在Next.js文档[1](而不是React
文档)中看到。
useOptimistic
主要用来优化「提交数据」的用户体验。
比如,在「点赞」的场景,通常逻辑是:
但为了用户体验的流畅,前端通常会把逻辑做成:
useOptimistic
的本质就是在状态层面实现上述效果。
useFormStatus
则用于在表单提交过程中显示pending
状态:
function ButtonDisabledWhilePending({action, children}) {
const {pending} = useFormStatus();
return (
<button disabled={pending} formAction={action}>
{children}
</button>
);
}
有同学可能会疑惑:useFormStatus
没有传参,他怎么知道对应哪个form
?
实际上,为了实现useFormStatus
,React
在源码内为所有HostComponent
(即原生HTML
元素对应组件,比如<div/>
)定制了一个context
。
当某个form
触发表单提交时,context
的值会被更新为这个form
的数据。useFormStatus
本身仅仅是useContext(上述context)
。
可以发现,不管是useFormStatus
、useOptimistic
还是最近1~2年新出的hook
(比如useId
、useMutableSource
),我们开发者都很少会用到。
因为这些hook
都是为上层框架(主要是Next.js
)提供的。
React
早已完成他作为前端框架的使命。在他生命的后半程,他将作为上层框架的「操作系统」而存在。
server action
是Next.js
的未来,Next.js
是React
的未来。所以,React
的未来会围绕form
元素持续布局。
[1]
Next.js文档: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#experimental-useoptimistic