每个React开发者迟早都要处理表单。下面的教程将给你一个关于React中表单的全面概述。
你将学习如何在React中管理表单状态,受控和非受控表单的区别(状态与引用),如何提交表单(例如回调处理程序),以及如何重置表单(例如提交后)。此外,你还将学习一些高级话题,如React表单中的脏字段和验证。
在学习如何在没有任何表单库的情况下在React中实现这些高级主题的同时,你将了解到表单库如何为你执行这些任务。最终你会自己使用这样的表单库,例如React Hook Form,来为你完成这些更高级的任务。
React 表单实例
现在网络应用中的一个常见的表单例子是登录表单。它允许用户访问应用程序的认证和授权。在React中,我们将使用一个功能组件来表示这种表单:
import * as React from 'react';
const LoginForm = () => { return ( <form> <div> <label htmlFor="email">Email</label> <input id="email" type="text" /> </div> <div> <label htmlFor="password">Password</label> <input id="password" type="password" /> </div> <button>Submit</button> </form> );};
export { LoginForm };
该表单组件显示两个HTML输入字段,每个字段都有一个附加的HTML标签元素。所有的元素都在一个HTML表单元素中使用。
出于对表单可访问性的考虑,HTML标签元素可以使用一个htmlFor 属性(React专用),该属性链接到HTML输入元素的id 属性。当点击表单的标签时,相应的输入字段应该被聚焦。
请注意,该表单还没有收到任何道具。以后它可能会收到初始状态的道具,这些道具会填充表单或回调处理程序,在点击表单的提交按钮时被执行。
带有onSubmit的React表单
当用户点击表单的提交按钮时,我们可以使用HTML表单元素的onSubmit属性来给它附加一个事件处理程序。为了告诉表单,该按钮应该启动表单的事件处理程序,该按钮必须具有提交类型:
import * as React from 'react';
const LoginForm = () => { const handleSubmit = (event) => { event.preventDefault(); };
return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input id="email" type="text" /> </div> <div> <label htmlFor="password">Password</label> <input id="password" type="password" /> </div> <button type="submit">Submit</button> </form> );};
export { LoginForm };
为了防止本地浏览器的行为(会对浏览器进行刷新),我们可以在表单的事件上使用preventDefault() 方法。
不受控制的 React 表单
当提交一个表单时,我们想从表单中读取值。在React中,我们可以通过对HTML元素的引用来获得对它们的访问。因此,只要我们想在JSX中访问一个HTML元素,我们就会使用React的useRef Hook:
import * as React from 'react';
const LoginForm = () => { const emailRef = React.useRef(); const passwordRef = React.useRef();
const handleSubmit = (event) => { event.preventDefault();
const email = emailRef.current.value const password = passwordRef.current.value
alert(email + ' ' + password); };
return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input id="email" type="text" ref={emailRef} /> </div> <div> <label htmlFor="password">Password</label> <input id="password" type="password" ref={passwordRef} /> </div> <button type="submit">Submit</button> </form> );};
export { LoginForm };
当采取非控制形式的方法时,给每个表单字段附加一个参考可能太麻烦了。懒惰的方法是直接从表单的事件中读取表单值,因为表单知道它的元素和各自的值:
import * as React from 'react';
const LoginForm = () => { const handleSubmit = (event) => { event.preventDefault();
const email = event.target.elements.email.value; const password = event.target.elements.password.value;
alert(email + ' ' + password); };
return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input id="email" type="text" /> </div> <div> <label htmlFor="password">Password</label> <input id="password" type="password" /> </div> <button type="submit">Submit</button> </form> );};
export { LoginForm };
如果我们有一个直接的表单,我们不需要摆弄表单的状态,我们可以采用非控制表单的方法。然而,更习惯的React方式是使用受控表单。
受控的React表单
在React中使用表单的习惯性方式是使用React的声明性特性。我们将使用React的useState Hook来自己管理表单的状态。通过更新这个状态与每个输入字段的onChange 处理程序,我们可以通过传递给每个输入字段来分别使用这个状态(这里:email 和password )。这样,每个输入字段就被React控制了,而不再管理其内部的原生HTML状态了:
import * as React from 'react';
const LoginForm = () => { const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState('');
const handleEmail = (event) => { setEmail(event.target.value); };
const handlePassword = (event) => { setPassword(event.target.value); };
const handleSubmit = (event) => { event.preventDefault();
alert(email + ' ' + password); };
return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input id="email" type="text" value={email} onChange={handleEmail} /> </div> <div> <label htmlFor="password">Password</label> <input id="password" type="password" value={password} onChange={handlePassword} /> </div> <button type="submit">Submit</button> </form> );};
export { LoginForm };
一旦表单变大,你会发现它有太多的处理程序来管理每个表单字段的状态。那么你可以使用下面的策略:
import * as React from 'react';
const LoginForm = () => { const [form, setForm] = React.useState({ email: '', password: '', });
const handleChange = (event) => { setForm({ ...form, [event.target.id]: event.target.value, }); };
const handleSubmit = (event) => { event.preventDefault();
alert(form.email + ' ' + form.password); };
return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input id="email" type="text" value={form.email} onChange={handleChange} /> </div> <div> <label htmlFor="password">Password</label> <input id="password" type="password" value={form.password} onChange={handleChange} /> </div> <button type="submit">Submit</button> </form> );};
export { LoginForm };
该策略将所有的表单状态统一为一个对象,所有的事件处理程序统一为一个处理程序。通过利用每个表单字段的标识符,我们可以在统一的处理程序中使用它,通过将标识符作为动态键来更新状态。
这可以很好地扩展React中的受控表单,因为状态、处理程序和表单字段不再是1:1:1的关系。相反,每个处理程序可以重用状态和处理程序。
受控表单与非受控表单
在实践中,关于React中的非控制表单与控制表单的讨论并不多。如果表单很简单,我们可以使用非控制式表单。然而,一旦表单有了更多的要求(例如对状态的控制),你就必须使用受控表单。
下面的表单例子说明了如何在提交操作后重置表单:
const LoginForm = () => { const [form, setForm] = React.useState({ email: 'john@doe.com', password: 'geheim', });
const handleChange = (event) => { setForm({ ...form, [event.target.id]: event.target.value, }); };
const handleSubmit = (event) => { event.preventDefault();
setForm({ email: '', password: '', }); };
return (...);};
虽然受控表单在React中更受欢迎,因为它们允许你在管理表单的状态(如初始状态、更新状态)方面有更好的开发者体验,但它们的性能更强。状态的每一次改变都会带来表单的重新渲染。对于一个不受控制的表单,没有重新渲染的过程。无论如何,大多数时候,这种性能影响不会被任何用户察觉。
提交一个React表单
你已经看到了如何在React中为一个表单创建一个提交按钮。到目前为止,我们只是触发了这个按钮并使用了它的附加事件处理程序,但我们还没有发送任何表单数据。通常情况下,一个表单组件会从使用表单数据的父组件那里接收一个回调处理程序。
const LoginForm = ({ onLogin }) => { const [form, setForm] = React.useState({ email: '', password: '', });
const handleChange = (event) => { setForm({ ...form, [event.target.id]: event.target.value, }); };
const handleSubmit = (event) => { event.preventDefault();
onLogin(form); };
return (...);};
这个例子显示了表单状态是如何作为表单数据传递给回调处理程序的。因此,一旦用户点击了提交按钮,父组件将接收表单数据并对其执行任务(例如,将表单数据发布到后台)。
React表单重置
之前你已经看到过一个表单重置的例子。然而,在前面的例子中,我们在表单状态中逐一重置了每个表单字段(例如,电子邮件和密码)。然而,如果我们从一开始就提取表单状态作为初始状态,我们就可以重新使用这个初始状态进行重置:
const INITIAL_STATE = { email: '', password: '',};
const LoginForm = ({ onLogin }) => { const [form, setForm] = React.useState(INITIAL_STATE);
...
const handleSubmit = (event) => { event.preventDefault();
// call your component's callback handler, e.g. onLogin
setForm(INITIAL_STATE); };
return (...);};
在React中处理表单时,将初始表单状态提取为变量往往是有意义的。重置只是这种方法取得成果的一个有价值的例子。
React表单模板
前面的例子已经给了你许多复制和粘贴的模板,让你开始在React中使用表单。然而,到目前为止,我们只在React表单中使用了两个HTML输入元素。还有许多其他的表单字段,你可以作为可重用的组件加入。
你已经看到了React中表单的基本用法。接下来我们将通过一些更高级的表单概念来说明表单的复杂性。当你通过这些概念时,你会学到如何实现这些高级概念,然而,请注意,最终会有一个专门的表单库来处理这些实现的问题。
React 表单脏污
如果一个表单的一个字段被用户改变了,那么这个表单就是脏的。在使用表单时,dirty状态有助于处理某些场景。例如,只有当一个表单字段被改变时,提交按钮才应该被启用:
const INITIAL_STATE = { email: '', password: '',};
const getDirtyFields = (form) => Object.keys(form).reduce((acc, key) => { // check all form fields that have changed const isDirty = form[key] !== INITIAL_STATE[key];
return { ...acc, [key]: isDirty }; }, {});
const LoginForm = ({ onLogin }) => { const [form, setForm] = React.useState(INITIAL_STATE);
...
const dirtyFields = getDirtyFields(form);
const hasChanges = Object.values(dirtyFields).every( (isDirty) => !isDirty );
return ( <form onSubmit={handleSubmit}> ... <button disabled={hasChanges} type="submit"> Submit </button> </form> );};
前面的代码片断显示了建立一个计算状态的实现,它知道每个表单字段的脏污状态。然而,这已经显示了自己管理这个脏状态的复杂性。因此,我建议使用React Hook Form这样的表单库。
带有验证功能的React表单
使用表单库的最常见的罪魁祸首是表单的验证。虽然下面的实现看起来很直接,但有很多移动的部分会进入一个复杂的验证中。所以,跟着我学习如何自己执行这样的任务,但不要犹豫,最终还是要使用表单库来完成。
const INITIAL_STATE = { email: '', password: '',};
const VALIDATION = { email: [ { isValid: (value) => !!value, message: 'Is required.', }, { isValid: (value) => /\S+@\S+\.\S+/.test(value), message: 'Needs to be an email.', }, ], password: [ { isValid: (value) => !!value, message: 'Is required.', }, ],};
const getErrorFields = (form) => Object.keys(form).reduce((acc, key) => { if (!VALIDATION[key]) return acc;
const errorsPerField = VALIDATION[key] // get a list of potential errors for each field // by running through all the checks .map((validation) => ({ isValid: validation.isValid(form[key]), message: validation.message, })) // only keep the errors .filter((errorPerField) => !errorPerField.isValid);
return { ...acc, [key]: errorsPerField }; }, {});
const LoginForm = ({ onLogin }) => { const [form, setForm] = React.useState(INITIAL_STATE);
...
const errorFields = getErrorFields(form); console.log(errorFields);
return (...);};
通过将每个表单字段的所有错误作为计算属性,我们可以执行一些任务,比如在有验证错误时阻止用户提交表单。
const LoginForm = ({ onLogin }) => { ...
const handleSubmit = (event) => { event.preventDefault();
const hasErrors = Object.values(errorFields).flat().length > 0; if (hasErrors) return;
// call your component's callback handler, e.g. onLogin };
return (...);};
几乎更重要的是向用户显示关于表单错误的反馈。因为我们有所有的错误,我们可以在JSX中作为提示有条件地显示它们。
const LoginForm = () => { ...
const errorFields = getErrorFields(form);
return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input id="email" type="text" value={form.email} onChange={handleChange} /> {errorFields.email?.length ? ( <span style={{ color: 'red' }}> {errorFields.email[0].message} </span> ) : null} </div> <div> <label htmlFor="password">Password</label> <input id="password" type="password" value={form.password} onChange={handleChange} /> {errorFields.password?.length ? ( <span style={{ color: 'red' }}> {errorFields.password[0].message} </span> ) : null} </div> <button type="submit">Submit</button> </form> );};
在React中对表单处理的学习越来越多,揭示了某些主题随着时间的推移变得多么复杂。我们在这里只触及了表面。学习引擎下的一切工作原理是很好的,因此本教程介绍了React中的表单,然而,最终你应该选择像React Hook Form这样的表单库。
表单库React Hook Form
我现在最常用的表单库是React Hook Form。可以说它是一个无头的表单库,因为它不带有任何表单组件,而只是带有自定义的React Hooks,允许你访问表单状态、dirty状态和验证状态。但还有更多。在第三方UI库中的集成,高性能的重新渲染,表单观察器,以及第三方验证库的使用,如Yup和Zod。