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

React Form

在构建 web 应用的时候,为了采集用户输入,表单变成了咱们不可或缺的东西。大型项目中,若是没有对表单进行很好的抽象和封装,随着表单复杂度和数量的增长,处理表单将会变成一件使人头疼的事情。在 react 里面处理表单,一开始也并不容易。因此在这篇文章中,咱们会介绍一些简单的实践,让你可以在 react 里面更加轻松的使用表单。若是你对 HTML 表单的基础掌握得不是太好,那么我建议你先阅读个人上一篇文章 深刻理解 HTML 表单html

好了,废话很少说,让咱们先来看一个简单的例子。react

示例

LoginForm.jsgit

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 以后,提交用户输入。github

移步 这里
查看文章中的所有代码

数据的抽象

对于每个表单元素来讲, 除开 DOM 结构的不同,初始值, 错误信息, 是否被 touched, 是否 valid,这些数据都是必不可少的。因此,咱们能够抽象一个中间组件,将这些数据统一管理起来,而且适应不一样的表单元素。这样 Field 组件 就应运而生了。web

Field 做为一个中间层,包含表单元素的各类抽象。最基本的就是 Field 的名字对应的值
Field 不能单独存在,由于 Field 的 value 都是来自传入组件的 state, 传入组件经过 setState 更新 state, 使 Field 的 value 发生变化redux

Field: {
   name: String,  // filed name, 至关于上面提到的 key
   value: String, // filed value
}

在实际状况中, 还须要更多的数据来控制 Field 的表现行为,好比 valid, invalid, touched 等。segmentfault

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 的抽象api

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 变得更好用。

相关文章
相关标签/搜索