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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom