姓氏
电子邮件
);
}
这个组件使用了集中式的`useState`对象,而这正是大多数开发者被教导用来构建表单的标准方法。一旦任何输入字段的值发生变化,状态就会立即得到更新。
在 React 中,每当您更新状态时,组件都会重新渲染以显示当前的状态值。这就会导致性能问题,而您需要解决这个问题。
下图清楚地展示了这样一个性能问题:每当用户在输入框中输入一个字符时,相关表单组件就会重新渲染。
不受控制的“ref”组件的解决方法
鉴于我们在使用受控表单组件时遇到的这些问题,开发人员找到了一种解决办法。如果我们不使用状态,而是使用“ref”,那么当“ref”的当前值发生变化时,组件就不会重新渲染。这个方法听起来似乎很不错,但事实上它会带来完全不同的架构问题。
以下是使用“ref”而不是状态来管理相同表单的示例:
import { useRef } from "react";
export function RefRegistrationForm() {
const firstNameRef = useRef(null);
const lastNameRef = useRef(null);
const emailRef = useRef(null);
console.log("表单已渲染(注意,在输入内容时控制台不会输出任何信息)");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = {
firstName: firstNameRef.current?.value,
lastName: lastNameRef.current?.value,
email: emailRef.current?.value,
};
console.log("提交的数据:", formData);
};
return (
“ref”组件的解决方法
这种方法速度很快,但扩展性很差。
);
}
让我们来谈谈这种方法存在的问题:
“引用对象”的问题:我们为每一个输入字段都创建了一个单独的引用对象。想象一下,如果这个表单包含20个以上的字段,那你将不得不管理所有这些引用对象!
我们需要手动提取数据,然后再将这些数据重新组合成完整的对象。这里并没有像`setState()`这样的机制可供使用。
如何实现实时验证呢?由于在用户输入内容时,相关的字段或组件并不会重新渲染,因此如果不引入新的状态变量,要想在输入框下方显示动态错误信息就会变得非常困难;而引入新的状态变量又会违背使用引用对象的初衷。
没错!我们也不应该忽略这个选项。既然有这样的可能性,为什么还要使用第三方库呢?我们不能直接使用原生的HTML表单,在用户提交表单时再通过`FormData`来获取数据吗?
如果只是构建简单的“联系我们”或“登录表单”,那么原生的HTML表单确实是最适合的工具。它们结构简单,不需要了解任何第三方库的知识,而且效率也非常高。
但对于那些复杂、用于生产环境的Web应用程序来说,原生方法很快就会遇到瓶颈:
复杂的数据结构:`FormData`是一个扁平化的对象。如果你需要处理深度嵌套的对象或动态数组,那么从扁平化的`FormData`对象中手动提取像`speakers[0][firstName]`这样的数据会非常麻烦。
动态的用户界面:如果你的应用有这样的需求:只有当字段A的值被设置为“是”时,字段B才会显示出来,那么你仍然需要使用客户端的状态来跟踪字段A的值。而`FormData`只在表单提交的那一刻才存在。
用户体验与实时验证:原生的HTML表单提供的验证功能较为僵化,在不同浏览器中很难实现统一的样式效果。如果你需要进行复杂的跨字段验证(比如确认密码是否一致),或者需要异步进行验证(比如在用户输入用户名时检查该用户名是否已被占用),那么你就需要使用JavaScript了。
当原生表单无法满足需求时,我们就转向React来解决问题。
TanStack Form是一个专为处理用户输入而设计的无状态框架。它提供了严格的类型检查以及便捷的数据管理功能,同时通过细粒度的反应式机制来确保只有你正在编辑的字段才会被重新渲染,其他部分则保持不变。
让我们在现代的Vite + TypeScript环境中配置它吧。首先,使用以下命令安装TanStack Form:
npm install @tanstack/react-form # 或者使用等效的yarn或pnpm命令
你不需要使用一大堆单独的状态管理钩子,只需要使用`useForm`这个钩子就能启动表单处理机制:
import { useForm } from '@tanstack/react-form';
export function SpeakerForm() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
twitterHandle: '',
},
onSubmit: async ({ value }) => {
console.log('表单已提交!', value);
},
});
return (
添加新的演讲者
{/* 我们将在这里构建表单字段 */}
);
}
请注意`defaultValues`这个对象。TanStack Form会自动根据这个对象来推断出整个表单的结构。如果你后来尝试引用一个无效的字段名称,TypeScript会立即抛出错误。
细粒度的反应性
现在,让我们将表单输入元素与处理引擎连接起来。为此,我们需要使用TanStack的`Field`组件来包裹这些输入元素。这个组件采用了标准的JSX `render prop`模式,从而能够将各个字段的状态直接传递到用户界面中。
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form handleSubmit();
}}
className="space-y-4"
>
<form.Field name="firstName">
{(field) => {
// 证明:只有当“firstName”字段的值发生变化时,这条日志才会被输出!
console.log("正在渲染‘First Name’字段");
return (
);
}}
</form.Field>
)}
</form.Field>
要添加数据,我们调用 field.pushValue();要删除数据,则调用 field.removeValue(index)。在新增行时,之前的数组字段内容保持不变,这种处理方式性能非常高。
上图展示了这种多演讲者信息表单及其提交后的显示效果。
有时,你需要在输入框之外访问表单的状态。例如,当用户更改输入字段的值时,你希望立即显示“未保存的更改!”提示信息。
需要注意的是,这里的提示信息虽然位于表单之外,但它完全取决于表单字段的值。
TanStack为这类需求提供了两种不同的工具,知道何时使用哪种工具是掌握这种架构的关键。
使用useSelector()
如果状态的变化会影响到页面的整体布局(比如像上面那样显示一个“未保存的更改”提示栏),那么你就需要让父组件重新渲染。这时我们可以使用独立的useSelector()钩子来实现这一目标:
import { useForm, useSelector } from "@tanstack/react-form";
export function ReactivityDemo() {
const form = useForm({ defaultValues: { bio: "" } });
// 这里只选取我们希望父组件跟踪的那部分状态
const isFormDirty = useSelector(form.store, (state) => state.isDirty);
return (
)}
{/* 表单内容放在这里 */}
);
}
如果状态的变化只影响某个特定的交互行为,比如在表单正在保存时禁用提交按钮,那么我们就不需要让整个表单重新渲染。在这种情况下,我们可以将相关代码放在内部:
>
{([canSubmit, isSubmitting]) => (
{isSubmitting ? "保存中..." : "保存更改"}
)}
这样,只有提交按钮会重新渲染,组件的其他部分则保持不变。
如果你已经从事React应用程序的开发有一段时间了,你可能会想:“为什么不直接使用React Hook Form呢?”
React Hook Form确实是一个非常优秀的库,多年来一直被视为行业标准。它通过利用不受控制的组件和useRef来解决性能问题,能够完美地实现状态变化的隔离处理,并且也能很好地处理验证逻辑。
但是,当应用程序的复杂性增加时,TanStack Form架构相比React Hook Form具有三个明显的优势:
动态数组
如果你曾经使用React Hook Form构建过需要动态嵌套数组的复杂表单(比如在上面的例子中添加多个扬声器),那么你很可能需要像useFieldArray这样的钩子。管理复杂的useFieldArray实现通常会涉及大量的样板代码、精确的索引追踪,以及为确保TypeScript类型安全而进行的深入处理。
TanStack Form消除了这些麻烦。它将数组视为与字符串输入相同的类型进行处理。该表单实际上是一个集中式的状态机:你只需告诉某个字段其mode=array,就能立即使用pushValue()和removeValue()方法,并且还能享受到深度嵌套结构下的类型安全保障。
深度嵌套组件
React Hook Form依靠非受控输入来提升性能。虽然这种方式运行速度很快,但在深度嵌套的组件结构中传递引用却会变得很复杂,尤其是当与复杂的UI库集成时。
TanStack Form为我们提供了受控组件的严谨且可预测的架构,并通过细粒度的反应式机制确保只有真正发生变化的UI部分才会被重新渲染。
生态系统的协同性
如果你正在使用TanStack Query进行数据获取,同时使用TanStack Router进行导航,那么TanStack Form与你所使用的其他技术有着完全相同的思维模式和架构体系。它能够无缝融入整个生态系统,为你的应用程序架构提供统一的开发体验。
结论与重要资源
通过结合使用TanStack Form、Zod以及ShadCN UI,我们构建了一种类型严格、具有反应式特性的、可用于生产环境的表单架构。这种架构能够处理从基本文本输入到复杂嵌套数组等各种类型的数据,而且丝毫不会影响性能。
你可以在我的GitHub仓库中获取完整的入门代码和最终项目源码:
如果你觉得这些内容有帮助,那么以下这两段深入的视频教程也会对你很有帮助:
如果你已经读到了这里……
非常感谢你!
我很高兴地宣布,我刚刚开始制作了一门全栈免费课程,旨在帮助开发者从基础编程知识逐步掌握适用于生产环境的开发思维模式。如果你能抽时间观看并参与其中,我会感到非常荣幸。
期待在下一篇文章中与大家见面。在此之前,祝大家一切安好,继续努力学习吧!