在 React 中使用表单有个明显的痛点,就是须要维护大量的value
和onChange
,好比一个简单的登陆框:javascript
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
username: "",
password: ""
};
}
onUsernameChange = e => {
this.setState({ username: e.target.value });
};
onPasswordChange = e => {
this.setState({ password: e.target.value });
};
onSubmit = () => {
const data = this.state;
// ...
};
render() {
const { username, password } = this.state;
return (
<form onSubmit={this.onSubmit}>
<input value={username} onChange={this.onUsernameChange} />
<input
type="password"
value={password}
onChange={this.onPasswordChange}
/>
<button>Submit</button>
</form>
);
}
}
复制代码
这已是比较简单的登陆页,一些涉及到详情编辑的页面,十多二十个组件也是常有的。一旦组件多起来就会有许多弊端:java
setState
的使用,会致使从新渲染,若是子组件没有相关优化,至关影响性能。总结起来,做为一个开发者,迫切但愿能有一个表单组件可以同时拥有这样的特性:react
表单组件社区上已经有很多方案,例如react-final-form、formik,ant-plus、noform等,许多组件库也提供了不一样方式的支持,如ant-design。git
但这些方案都或多或少一些重量,又或者使用方法仍然不够简便,天然造轮子才是最能复合要求的选择。github
这个表单组件实现起来主要分为三部分:npm
Form
:用于传递表单上下文。Field
: 表单域组件,用于自动传入value
和onChange
到表单组件。FormStore
: 存储表单数据,封装相关操做。为了能减小使用ref
,同时又能操做表单数据(取值、修改值、手动校验等),我将用于存储数据的FormStore
,从Form
组件中分离出来,经过new FormStore()
建立并手动传入Form
组件。数组
使用方式大概会长这样子:并发
class App extends React.Component {
constructor(props) {
super(props);
this.store = new FormStore();
}
onSubmit = () => {
const data = this.store.get();
// ...
};
render() {
return (
<Form store={this.store} onSubmit={this.onSubmit}> <Field name="username"> <input /> </Field> <Field name="password"> <input type="password" /> </Field> <button>Submit</button> </Form> ); } } 复制代码
用于存放表单数据、接受表单初始值,以及封装对表单数据的操做。ide
class FormStore {
constructor(defaultValues = {}, rules = {}) {
// 表单值
this.values = defaultValues;
// 表单初始值,用于重置表单
this.defaultValues = deepCopy(defaultValues);
// 表单校验规则
this.rules = rules;
// 事件回调
this.listeners = [];
}
}
复制代码
为了让表单数据变更时,可以响应到对应的表单域组件,这里使用了订阅方式,在FormStore
中维护一个事件回调列表listeners
,每一个Field
建立时,经过调用FormStore.subscribe(listener)
订阅表单数据变更。函数
class FormStore {
// constructor ...
subscribe(listener) {
this.listeners.push(listener);
// 返回一个用于取消订阅的函数
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) this.listeners.splice(index, 1);
};
}
// 通知表单变更,调用全部listener
notify(name) {
this.listeners.forEach(listener => listener(name));
}
}
复制代码
再添加get
和set
函数,用于获取和设置表单数据。其中,在set
函数中调用notify(name)
,以保证全部的表单变更都会触发通知。
class FormStore {
// constructor ...
// subscribe ...
// notify ...
// 获取表单值
get(name) {
// 若是传入name,返回对应的表单值,不然返回整个表单的值
return name === undefined ? this.values : this.values[name];
}
// 设置表单值
set(name, value) {
//若是指定了name
if (typeof name === "string") {
// 设置name对应的值
this.values[name] = value;
// 执行表单校验,见下
this.validate(name);
// 通知表单变更
this.notify(name);
}
// 批量设置表单值
else if (name) {
const values = name;
Object.keys(values).forEach(key => this.set(key, values[key]));
}
}
// 重置表单值
reset() {
// 清空错误信息
this.errors = {};
// 重置默认值
this.values = deepCopy(this.defaultValues);
// 执行通知
this.notify("*");
}
}
复制代码
对于表单校验部分,不想考虑得太复杂,只作一些规定
FormStore
构造函数中传入的rules
是一个对象,该对象的键对应于表单域的name
,值是一个校验函数
。校验函数
参数接受表单域的值和整个表单值,返回boolean
或string
类型的结果。true
表明校验经过。false
和string
表明校验失败,而且string
结果表明错误信息。而后巧妙地经过||
符号判断是否校验经过,例如:
new FormStore({/* 初始值 */, {
username: (val) => !!val.trim() || '用户名不能为空',
password: (val) => !!(val.length > 6 && val.length < 18) || '密码长度必须大于6个字符,小于18个字符',
passwordAgain: (val, vals) => val === vals.password || '两次输入密码不一致'
}})
复制代码
在FormStore
实现一个validate
函数:
class FormStore {
// constructor ...
// subscribe ...
// notify ...
// get
// set
// reset
// 用于设置和获取错误信息
error(name, value) {
const args = arguments;
// 若是没有传入参数,则返回错误信息中的第一条
// const errors = store.error()
if (args.length === 0) return this.errors;
// 若是传入的name是number类型,返回第i条错误信息
// const error = store.error(0)
if (typeof name === "number") {
name = Object.keys(this.errors)[name];
}
// 若是传了value,则根据value值设置或删除name对应的错误信息
if (args.length === 2) {
if (value === undefined) {
delete this.error[name];
} else {
this.errors[name] = value;
}
}
// 返回错误信息
return this.errors[name];
}
// 用于表单校验
validate(name) {
if (name === undefined) {
// 遍历校验整个表单
Object.keys(this.rules).forEach(n => this.validate(n));
// 并通知整个表单的变更
this.notify("*");
// 返回一个包含第一条错误信息和表单值的数组
return [this.error(0), this.get()];
}
// 根据name获取校验函数
const validator = this.rules[name];
// 根据name获取表单值
const value = this.get(name);
// 执行校验函数获得结果
const result = validator ? validator(name, this.values) : true;
// 获取并设置结果中的错误信息
const message = this.error(
name,
result === true ? undefined : result || ""
);
// 返回Error对象或undefind,和表单值
const error = message === undefined ? undefined : new Error(message);
return [error, value];
}
}
复制代码
至此,这个表单组件的核心部分FormStore
已经完成了,接下来就是这么在Form
和Field
组件中使用它。
Form
组件至关简单,也只是为了提供一个入口和传递上下文。
props
接收一个FormStore
的实例,并经过Context
传递给子组件(即Field
)中。
const FormStoreContext = React.createContext();
function Form(props) {
const { store, children, onSubmit } = props;
return (
<FormStoreContext.Provider value={store}> <form onSubmit={onSubmit}>{children}</form> </FormStoreContext.Provider> ); } 复制代码
Field
组件也并不复杂,核心目标是实现value
和onChange
自动传入到表单组件中。
// 从onChange事件中获取表单值,这里主要应对checkbox的特殊状况
function getValueFromEvent(e) {
return e && e.target
? e.target.type === "checkbox"
? e.target.checked
: e.target.value
: e;
}
function Field(props) {
const { label, name, children } = props;
// 拿到Form传下来的FormStore实例
const store = React.useContext(FormStoreContext);
// 组件内部状态,用于触发组件的从新渲染
const [value, setValue] = React.useState(
name && store ? store.get(name) : undefined
);
const [error, setError] = React.useState(undefined);
// 表单组件onChange事件,用于从事件中取得表单值
const onChange = React.useCallback(
(...args) => name && store && store.set(name, valueGetter(...args)),
[name, store]
);
// 订阅表单数据变更
React.useEffect(() => {
if (!name || !store) return;
return store.subscribe(n => {
// 当前name的数据发生了变更,获取数据并从新渲染
if (n === name || n === "*") {
setValue(store.get(name));
setError(store.error(name));
}
});
}, [name, store]);
let child = children;
// 若是children是一个合法的组件,传入value和onChange
if (name && store && React.isValidElement(child)) {
const childProps = { value, onChange };
child = React.cloneElement(child, childProps);
}
// 表单结构,具体的样式就不贴出来了
return (
<div className="form"> <label className="form__label">{label}</label> <div className="form__content"> <div className="form__control">{child}</div> <div className="form__message">{error}</div> </div> </div>
);
}
复制代码
因而,这个表单组件就完成了,愉快地使用它吧:
class App extends React.Component {
constructor(props) {
super(props);
this.store = new FormStore();
}
onSubmit = () => {
const data = this.store.get();
// ...
};
render() {
return (
<Form store={this.store} onSubmit={this.onSubmit}> <Field name="username"> <input /> </Field> <Field name="password"> <input type="password" /> </Field> <button>Submit</button> </Form> ); } } 复制代码
这里只是把最核心的代码整理了出来,功能上固然比不上那些成百上千 star 的组件,可是用法上足够简单,而且已经能应对项目中的大多数状况。
我已在此基础上完善了一些细节,并发布了一个 npm 包——@react-hero/form
,你能够经过npm安装,或者在github上找到源码。若是你有任何已经或建议,欢迎在评论或 issue 中讨论。