React表单

312 阅读2分钟

受控表单

状态变化交由React处理

const App = () => {
  const [value, setValue] = useState(0)
  const handleChange = (e) => { setValue(e.target.value) }

  return (
    <form onSubmit={() => {
      alert(value);
    }}>
      <input type="text" onChange={handleChange} />
      <button type="submit">submit</button>
    </form>
  )
};

如上述代码,input框监听onChange方法,每当数据改变时调用setValue进行数据更新。

非受控表单

状态变化通过ref引用获取

const App = () => {
  const input = useRef(null)

  return (
    <form onSubmit={() => {
      const data = input.current.value
      alert(data);
    }}>
      <input type="text" ref={input} />
      <br />
      <button type="submit">submit</button>
    </form>
  )
};

输入框的值不通过setState更新,而是从ref中获取。

实现表单组件

了解使用方式

以ArcoDesign为例

可以在**Form.Item传入 field**属性,即可使控件变为受控组件,表单项的值都将会被 Form 收集。

  1. 受控模式下 FormItem会接管控件,自动给表单控件添加相应的 valueonChange,所有的数据收集都由 Form 内部完成。
function Form() {
  return (
    <Form
      onSubmit={(v) => {
        console.log(v);
      }}
      style={{ width: 600 }}
    >
      <FormItem label='Username' field='name'>
        <Input />
      </FormItem>
      <FormItem label='Age' field='age'>
        <InputNumber />
      </FormItem>
      <FormItem>
        <Button type='primary' htmlType='submit'>
          Submit
        </Button>
      </FormItem>
    </Form>
  );
}

实现

useForm

  1. 通过 useForm() 获取 formInstance 实例,form表单的数据也在实例上
  1. formInstance 实例对外提供了全局的方法如 setFieldsValuegetFieldsValue
import React, { useRef } from "react";

class FormStore {
    // 存储表单数据
    store = {}
    // 获取单个字段值
    getFieldValue = (name) => {
        return this.store[name]
    }
    // 获取所有字段值
    getFieldsValue = () => {
        return this.store
    }
    // 设置字段的值
    setFieldsValue = (newStore) => {
        // 更新store的值
        this.store = {
            ...this.store,
            ...newStore,
        }
    }
    // 提供FormStore实例方法
    getForm = () => ({
        getFieldValue: this.getFieldValue,
        getFieldsValue: this.getFieldsValue,
        setFieldsValue: this.setFieldsValue,
    });
}
// 创建单例formStore
export default function useForm(form) {
    const formRef = useRef();
    if (!formRef.current) {
        if (form) {
            formRef.current = form;
        } else {
            const formStore = new FormStore();
            formRef.current = formStore.getForm();
        }
    }
    return [formRef.current]
}

Form

  1. formInstance实例进行传递
  1. 然后渲染子节点

创建公用的Context

const FieldContext = React.createContext({});

阻止onSubmit默认事件,并执行Form的onSubmit方法,传入最新的表单数据,将formInstance实例传递下去。

const Form = (props) => {
    const { form, onSubmit, children, ...restProps } = props;
    const [formInstance] = useForm(form) as any;
    return (
        <form
            {...restProps}
            onSubmit={(e) => {
                e.preventDefault()
                onSubmit(formInstance.getFieldsValue())
            }}>
            <FieldContext.Provider value={formInstance}>
                {children}
            </FieldContext.Provider>
        </form>
    )
}

FormItem

  1. 调用cloneElement给子组件添加valueonChange的props
  1. onChange时调用setFieldsValue更新表单数据
const Item = (props) => {
    const context = useContext(FieldContext);
    const { children } = props;
    // Field 中传进来的子元素变为受控组件,也就是主动添加上 value 和 onChange 属性方法
    const getControlled = () => {
        const { field } = props;
        const { getFieldValue, setFieldsValue } = context
        return {
            value: getFieldValue(field),
            onChange: (event: any) => {
                const newValue = event.target.value
                setFieldsValue({ [field]: newValue })
            },
        }
    }

    return cloneElement(children, getControlled())
}

Demo

import useForm from './useForm.js';
import React from 'react';
import './App.css';
import { Form, Item } from './Form.tsx';
const App = () => {
  const [form] = useForm();
  return (
    <Form
      form={form}
      onSubmit={(v) => {
        console.log(v);
      }}
    >
      <Item field="usename">
        <input />
      </Item>
      <Item>
        <button type="submit">submit</button>
      </Item>
    </Form>
  )
}

总结

整体来看,使用useForm创建实例,通过这个实例存储表单数据,通过给子组件注入props实现收集数据的功能。这里仅做了一个简单实现,还有许多特性有待拓展。