深入理解 form 系列(二)-- React 表单的优化

橘子小睿

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 name, 相当于上面提到的 key
   value: 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 变得更好用。

阅读 7.3k

橘子小睿的前端杂记
专注前端开发

前端开发者

657 声望
73 粉丝
0 条评论

前端开发者

657 声望
73 粉丝
文章目录
宣传栏