React.js -- 优化你的表单

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 组件

  1. 作为通用抽象,Field对外提供一致接口。 一致的接口能够使 Field 的使用起来更加的简单。比如更新 checkbox 的时候,我们更新的是它的 checked 属性而不是 value 属性,但是我们可以对 Field 进行封装,对外全部提供 value 属性,使开发变得更加容易。

  2. 作为中间层,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> 应该具有哪些特点?

  1. 通过 props 提供 value。可控组件并不维护自己的内部状态,也就是外部提供什么,就显示什么,所以组件能够通过 props 很好的控制起来

  2. 通过 onChange 更新value。

<input
      type="text"
      value={this.props.username}
      onChange={this.handleChange}
   />

点这里了解 => React 可控组件与不可控组件

使用 React 高阶组件进一步优化

在 LoinForm.js 中可以看到,我们对 setState 操作的依赖程度很高。如果在 form 中多添加一些 Field 组件,不难发现对于每一个 Field,都需要重复 setState 操作。过多的 setState 会我们的Form 组件变得不可控,增加维护成本。

仔细观察上面的代码,不难发现,在每一次 onChange 事件中,都是通过一个 keyvalue更新到 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>
    );
  }
}

通过 composewithStatewithHandler 组合起来,并应用到 Form 之后,跟之前比起来,LoginForm 已经简化了很多。LoginForm 不再自己维护内部状态,变成了一个完完全全的可控组件,不管是之后要对它写测试还是要重用它,都变得十分的轻松了。

点这里了解 => Recompose

结语

对于复杂的项目来说,以上的抽象还远远不够,在下一篇文章中,会介绍如何进一步让你的 Form 变得更好用。

相关文章

react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接...
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc ...