React —— Context API & HOC 实战

开始

Context API 的具体概念和用法请参考官方文档

HOC的基本概念也请自行查阅文档

本文要实现的目标是类似 antd4 的 form 表单

<Form
  form={form}
  onFinish={onFinish}
  onFinishFailed={onFinishFailed}
>
  <Field name="username" rules={[xxx]}>
	<Input placeholder={'xxx'}/>
  </Field>
</Form>

注意这里的 form 是通过一个方法获得的

const [form] = Form.useForm()

构想

我们要以使用方法开始

根据使用 antd 的经验,有时我们可能会在 <Form><Field> 之间嵌套 <Row><Col> 来设置样式,但是此时 Form 表单依然是能接受到其中 <Field> 的值的

从使用方式可以看出,我们要解决跨层级通信的这个问题,这个时候使用 React 的 Context API 是最合适的。但是 Context API 有一个问题就是一旦触发更新,相关的所有组件都会更新,尽管很多时候只是操作一个表单元素,其他没变的表单元素也会触发更新,这样会造成很大程度的性能浪费

为了解决这个问题,我们可以将 Form 实例 单独提取出来,通过单例模式实现全局一个 form,然后再通过观察者模式实现局部更新

以下描述顺序为从外向内


index.js

import _Form from './form'
import Field from './Field'
import useForm from './useForm'

const Form = _Form
Form.useForm = useForm

export {Field}
export default Form

Form.js

使用方式

<Form
  form={form}
  onFinish={onFinish}
  onFinishFailed={onFinishFailed}
>
  <Field name="username" rules={[xxx]}>
	<Input placeholder={'xxx'}/>
  </Field>
</Form>

Form.js

import React from "react";
import FieldContext, { SubmitContext } from "./FieldContext";

export default function Form({ form, children, onFinish, onFinishFailed }) {

  return (
    <form>
      <SubmitContext.Provider value={{ onFinish, onFinishFailed }}>
        <FieldContext.Provider value={form}>
          {children}
        </FieldContext.Provider>
      </SubmitContext.Provider>
    </form>
  );
}

FieldContext.js

import React from "react";

// 创建 Form Context对象
const FieldContext = React.createContext();
// 创建表单提交函数context
const SubmitContext = React.createContext();

export default FieldContext;

export {
  SubmitContext
}

Field.js

使用方式

<Form
  form={form}
  onFinish={onFinish}
  onFinishFailed={onFinishFailed}
>
  <Field name="username" rules={[xxx]}>
	<Input placeholder={'xxx'}/>
  </Field>
</Form>

Field.js 中要做以下几件事

  1. 给子组件赋予读写数据的能力
    这里的子组件指的是各种表单组件
  2. 注册组件更新方法
  3. 在组件挂载时将组件注册到 form context
import React, { Component } from "react";
import FieldContext from "./FieldContext";
import { verifyForm } from './hoc';

@verifyForm
class Field extends Component {
  static contextType = FieldContext;

  componentDidMount() {
    this.context.registerEntity(this);
  }

  onStoreChange = () => {
    this.forceUpdate();
  };

  getCntrolled = () => {
    const { getFieldValue, setFieldValue } = this.context;
    const { name } = this.props;

    return {
      value: getFieldValue(name), //"username", 从formStore当中读取数据
      onChange: e => {
        const newValue = e.target.value;
        // 使用验证函数
        this.props.verify(e.target.value);
        // 设置formStore的数据
        setFieldValue({ [name]: newValue });
      }
    };
  };

  render() {
    const { children } = this.props;
    
    return React.cloneElement(children, this.getCntrolled());
  }
}

export default Field;

我们通过 HOC 的形式向表单组件增加错误验证项

hoc.js

import React, { useCallback, useState } from 'react';

export function verifyForm(C) {
  return function (props) {
    const [errorMsgVisible, setErrorMsgVisible] = useState(false);
    
    const verify = useCallback((newValue) => {
      if (newValue === '') {
        setErrorMsgVisible(true);
      } else {
        setErrorMsgVisible(false);
      }
    }, []);

    return (
      <div>
        <C {...props} verify={verify} />
        {
          errorMsgVisible
            ? <p style={{ padding: '0 10px', color: 'red' }}>{props.rules[0] ? props.rules[0].message : '请输入参数'}</p>
            : null
        }
      </div>
    )
  }
}

这里只是简单做了一下是否为空的验证,实际上应该要遍历用户传入的 rules 进行规则验证


useForm.js

注册并返回单个 Form 实例

  • 注册组件方法
  • 获取表单值的方法
  • 改变表单值的方法
  • 获取所有表单值的方法
import React from 'react';

// 存储Form的数据
class FormStore {
  constructor() {
    // 这里存储Form要处理的数据
    this.store = {};
    // 源码当中用的是数组
    this.fieldEntities = {}; 
    
  }

  registerEntity = entity => {
  	this.fieldEntities = {
      ...this.fieldEntities,
      [entity.props.name]: entity
    };
  };

  getFieldValue = name => {
    const v = this.store[name];
    return v;
  };

  setFieldValue = newStore => {
    // step1: 修改store数据
    this.store = {
      ...this.store,
      ...newStore
    };

    // step2: 更新组件
    Object.keys(newStore).forEach(name => {
      this.fieldEntities[name].onStoreChange();
    });
  };

  getAllValues = () => {
    return {
      values: this.store,
      allFields: this.fieldEntities
    };
  }

  getForm = () => {
    return {
      getFieldValue: this.getFieldValue,
      setFieldValue: this.setFieldValue,
      registerEntity: this.registerEntity,
      getAllValues: this.getAllValues,
    };
  };
}

// 这里使用了单例模式
export default function useForm() {
  const formRef = React.useRef();

  if (!formRef.current) {
    const formStore = new FormStore();
    formRef.current = formStore.getForm();
  }

  return [formRef.current];
}


最后再补充一下关于提交表单的

先看一下使用方式

import React from "react";
import Form, { Field } from "../components/my-rc-field-form/";
import Input from "../components/Input";
import MyButton from '../components/MyButton';

const nameRules = { required: true, message: "请输入用户名!" };
const passwordRules = { required: true, message: "请输入密码!" };

export default function MyRCFieldForm(props) {
  const [form] = Form.useForm();

  const onFinish = val => {
    console.log("onFinish", val); 
  };

  // 表单校验失败执行
  const onFinishFailed = val => {
    console.log("onFinishFailed", val); 
  };

  return (
    <div>
      <h3>MyRCFieldForm</h3>
      <Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
        <Field name="username" rules={[nameRules]}>
          <Input placeholder="input UR Username" />
        </Field>
        <Field name="password" rules={[passwordRules]}>
          <Input placeholder="input UR Password" />
        </Field>
        <Field>
          <MyButton htmlType="submit" context="Submit" />
        </Field>
      </Form>
    </div>
  );
}

MyButton.js

import React, { useCallback, useContext } from 'react';
import FieldContext, { SubmitContext } from './my-rc-field-form/FieldContext';

export default function MyButton(props) {
  const fieldContext = useContext(FieldContext);
  const submitContext = useContext(SubmitContext);

  const handleClick = useCallback(() => {
    if (props.htmlType === 'submit') {
      const { values, allFields } = fieldContext.getAllValues();
      let hasFullValues = true;

      Object.keys(allFields).filter(f => {
        if (f === 'undefined') return false;

        if (!values[f]) hasFullValues = false;
        return true;
      });

      if (hasFullValues) {
        submitContext.onFinish(values);
      } else {
        submitContext.onFinishFailed(values);
      }
    }
  }, [props.htmlType, fieldContext, submitContext]);

  return (
    <div onClick={handleClick}>
      <div style={{
        width: '80px',
        padding: '5px 8px',
        border: '1px solid black',
        textAlign: 'center',
        cursor: 'pointer',
        userSelect: 'none',
      }}>{props.context}</div>
    </div>
  )
}

在这里既要引入 form context(用于获得表单数据)

也要引入 submit context (用于获取表单提交方法)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值