React Form
在构建 web 应用的时候,为了采集用户输入,表单变成了我们不可或缺的东西。大型项目中,如果没有对表单进行很好的抽象和封装,随着表单复杂度和数量的增加,处理表单将会变成一件令人头疼的事情。在 react 里面处理表单,一开始也并不容易。所以在这篇文章中,我们会介绍一些简单的实践,让你能够在 react 里面更加轻松的使用表单。如果你对 HTML 表单的基础掌握得不是太好,那么我建议你先阅读我的上一篇文章 深入理解 HTML 表单
好了,废话不多说,让我们先来看一个简单的例子。
示例
LoginForm.js
- handleChange = evt => {
- this.setState({
- username: evt.target.value,});
- };
- render() {
- return (
- <form>
- <label>
- username:
- <input
- type="text"
- name="username"
- value={this.state.username}
- onChange={this.handleChange}
- />
- </label>
- <input
- type="submit"
- value="Submit"
- />
- </form>
- );
- }
在上面的例子中,我们创建了一个输入框,期望用户在点击 submit 之后,提交用户输入。
数据的抽象
对于每一个表单元素来说,除开 DOM 结构的不一样,初始值,错误信息,是否被 touched,是否 valid,这些数据都是必不可少的。所以,我们可以抽象一个中间组件,将这些数据统一管理起来,并且适应不同的表单元素。这样 Field 组件 就应运而生了。
Field 作为一个中间层,包含表单元素的各种抽象。最基本的就是 Field 的名字 和 对应的值。
Field 不能单独存在,因为 Field 的 value 都是来自传入组件的 state,传入组件通过 setState 更新 state,使 Field 的 value 发生变化
- Field: {
- name: String,// filed name,相当于上面提到的 key
- value: String,// filed value
- }
在实际情况中,还需要更多的数据来控制 Field 的表现行为,比如 valid
,invalid
,touched
等。
- Field:{
- name: String,// filed value
- label: String,error: String,initialValue: String,valid: Boolean,invalid: Boolean,visited: Boolean,// focused
- touched: Boolean,// blurred
- active: Boolean,// focusing
- dirty: Boolean,// 跟初始值不相同
- pristine: Boolean,// 跟初始值相同
- component: Component|Function|String,// 表单元素
- }
点这里了解 => Redux Form 对 Field 的抽象
UI的抽象
Field 组件
作为通用抽象,Field对外提供一致接口。 一致的接口能够使 Field 的使用起来更加的简单。比如更新 checkBox 的时候,我们更新的是它的
checked
属性而不是value
属性,但是我们可以对 Field 进行封装,对外全部提供 value 属性,使开发变得更加容易。作为中间层,Field可以起到拦截作用。 如先格式化传入的 value,再将这个 value 传递给下层的组件,这样所有下层组件得到的都是格式化之后的值。
Field.js
- static defaultProps = {
- component: Input,};
- render() {
- const { component,noLabel,label,...otherProps } = this.props;
- return (
- <label>
- {!noLabel && <span>{label}</span>}
- {
- createElement(component,{ ...otherProps })
- }
- </label>
- );
- }
上面的例子是 Field 组件的简单实现。Field 对外提供了统一的 label 和 noLabel 接口,用来显示或不显示 label 元素。
Input 组件
创建Input 组件的关键点在于使它变得“可控”,也就是说它并不维护内部状态。关于可控组件,接下来会介绍。
Input.js
- handleChange = evt => {
- this.props.onChange(evt.target.value);
- };
- render() {
- return (
- <input {...this.props} onChange={this.handleChange} />
- );
- }
看上面的代码,为什么不直接把 onChange 函数通过 props 传进来呢?就像下面这样
- render() {
- return (
- <input {...this.props} onChange={this.props.onChange} />
- );
- }
其实是为了让我们从 onChange 回调中得到 统一的
value
,这样我们在外部就不用去 care 究竟是 取event.target.value
还是event.target.checked
.
优化后的 LoginForm 如下:
LoginForm.js
- class LoginForm extends Component {
- state = {
- username: '',};
- handleChange = value => {
- this.setState({
- username: value,});
- };
- render() {
- return (
- <form onSubmit={this.handleSubmit}>
- <Field
- label="username"
- name="username"
- value={this.state.username}
- onChange={this.handleChange}
- />
- <input
- type="submit"
- value="Submit"
- />
- </form>
- );
- }
- }
可控组件与不可控组件
可控组件与不可控组件最大的区别就是:对内部状态的维护与否。
一个可控的 <input> 应该具有哪些特点?
通过
props
提供 value。可控组件并不维护自己的内部状态,也就是外部提供什么,就显示什么,所以组件能够通过 props 很好的控制起来通过
onChange
更新value。
- <input
- type="text"
- value={this.props.username}
- onChange={this.handleChange}
- />
使用 React 高阶组件进一步优化
在 LoinForm.js 中可以看到,我们对 setState
操作的依赖程度很高。如果在 form 中多添加一些 Field 组件,不难发现对于每一个 Field,都需要重复 setState 操作。过多的 setState 会我们的Form 组件变得不可控,增加维护成本。
仔细观察上面的代码,不难发现,在每一次 onChange 事件中,都是通过一个 key
把 value
更新到 state
里面。比如上面的例子中,我们是通过 username
这个 key
去更新的。所以不难想到,利用高阶组件,可以不用在 LoginForm 里面维护内部状态。
高阶组件在这里就不再展开了,我会在接下来的文章中专门来详细介绍这一部分内容。
withState.js
- const withState = (stateName,stateUpdateName,initialValue) =>
- BaseComponent =>
- class extends Component {
- state = {
- stateValue: initialValue,};
- updateState = (stateValue) => {
- this.setState({
- stateValue,});
- };
- render() {
- const { stateValue } = this.state;
- return createElement(BaseComponent,{
- ...this.props,[stateName]: stateValue,[stateUpdateName]: this.updateState,});
- }
- };
除了 state 之外,我们可以将
onChange
,onSubmit
等事件处理函数也 extract 出去,这样可以进一步简化我们的 Form。
withHandlers.js
- const withHandlers = handlers => BaseComponent =>
- class WithHandler extends Component {
- cachedHandlers = {};
- handlers = mapValues(
- handlers,(createHandler,handlerName) => (...args) => {
- const cachedHandler = this.cachedHandlers[handlerName];
- if (cachedHandler) {
- return cachedHandler(...args);
- }
- const handler = createHandler(this.props);
- this.cachedHandlers[handlerName] = handler;
- return handler(...args);
- }
- );
- componentWillReceiveProps() {
- this.cachedHandlers = {};
- }
- render() {
- return createElement(BaseComponent,{
- ...this.props,...this.handlers,});
- }
- };
使用高阶组件改造后的 LoginForm 如下:
LoginForm.js
- const withLoginForm = _.flowRight(
- withState('username','onChange',''),withHandlers({
- onChange: props => value => {
- props.onChange(value);
- },onSubmit: props => event => {
- event.preventDefault();
- console.log(props.username);
- },})
- );
- @withLoginForm
- class LoginForm extends Component {
- static propTypes = {
- username: PropTypes.string,onChange: PropTypes.func,onSubmit: PropTypes.func,};
- render() {
- const { username,onChange,onSubmit } = this.props;
- return (
- <form onSubmit={onSubmit}>
- <Field
- label="username"
- name="username"
- value={username}
- onChange={onChange}
- />
- <input
- type="submit"
- value="Submit"
- />
- </form>
- );
- }
- }
通过
compose
把withState
和withHandler
组合起来,并应用到 Form 之后,跟之前比起来,LoginForm 已经简化了很多。LoginForm 不再自己维护内部状态,变成了一个完完全全的可控组件,不管是之后要对它写测试还是要重用它,都变得十分的轻松了。