Writing in the front
Antd should be familiar to those who use React development frequently. Most forms commonly encountered in development are completed by antD Form series components, and RC-field-Form is an important part of ANTD Form, or ANTD Form is a simple encapsulation of RC-field-Form.
I also encountered extremely complex form structures during the development process, and it was very painful to develop them in a common way. Therefore, I had a deep understanding of the working principle of RC-Field-Form, hoping to find a new way to simplify the development complexity by combining rC-Field-Form capabilities. This article shares my understanding of the internals of RC-Field-Form.
Rc-field-form has several important components: FormStore, Form component, and Field (FormItem) component. Using the React Context capability, these components are combined to form the basic functions of the form.
FormStore
Let’s talk about FormInstance before we talk about FormStore. We all know that when we call the useForm hook, we get an object that contains various form manipulation functions. We call this object a Form instance.
const [form] = Form.useForm();
Copy the code
FormStore is a class that stores Form data and defines various operations on that data. When we call useForm, an instance of FormStore is created internally and cached by useRef, which means that no matter how many times the component is rendered between the time the useForm component is created and the time it is destroyed, FormStore is only instantiated once.
/* useForm */
function useForm<Values = any> (form? : FormInstance
) :FormInstance<Values>] {
const formRef = React.useRef<FormInstance>();
const [, forceUpdate] = React.useState();
if(! formRef.current) {if (form) {
formRef.current = form;
} else {
// Create a new FormStore if not provided
const forceReRender = () = > {
forceUpdate({});
};
const formStore: FormStore = new FormStore(forceReRender); // instantiate FormStore
formRef.current = formStore.getForm(); // Get the Form instance}}return [formRef.current];
}
Copy the code
The resulting Form instance is actually the return value of formStore.getForm(), which exposes only the public methods and hides the formStore properties so they are safe for external use. Of course, if we pass in a form, we will return the form we passed directly.
/* formStore.getForm */
class FormStore {
/ /... Other properties and methods
public getForm = (): InternalFormInstance= > ({
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
getFieldError: this.getFieldError,
getFieldsError: this.getFieldsError,
isFieldsTouched: this.isFieldsTouched,
isFieldTouched: this.isFieldTouched,
isFieldValidating: this.isFieldValidating,
isFieldsValidating: this.isFieldsValidating,
resetFields: this.resetFields,
setFields: this.setFields,
setFieldsValue: this.setFieldsValue,
validateFields: this.validateFields,
submit: this.submit,
getInternalHooks: this.getInternalHooks,
});
private getInternalHooks = (key: string): InternalHooks | null= > {
if (key === HOOK_MARK) {
this.formHooked = true;
return {
dispatch: this.dispatch,
registerField: this.registerField,
useSubscribe: this.useSubscribe,
setInitialValues: this.setInitialValues,
setCallbacks: this.setCallbacks,
setValidateMessages: this.setValidateMessages,
getFields: this.getFields,
setPreserve: this.setPreserve,
};
}
warning(false.'`getInternalHooks` is internal usage. Should not call directly.');
return null;
};
}
Copy the code
I’m sure you’re familiar with these methods. These are the common ways we use Form instances on a daily basis, but they are slightly different from antD forms.
-
GetInternalHooks are not included in the antD Form exposed type definition. They are required by the RC-field-form Form component and field component. Antd hides them from the type, but they still exist. It’s just that we shouldn’t use it in our daily development.
-
The scrollToField method, which is implemented by ANTD, does not.
There are other properties and methods as well
interface Store {
[name: string] :any;
}
interfaceCallbacks<Values = any> { onValuesChange? :(changedValues: any, values: Values) = > void; onFieldsChange? :(changedFields: FieldData[], allFields: FieldData[]) = > void; onFinish? :(values: Values) = > void; onFinishFailed? :(errorInfo: ValidateErrorEntity<Values>) = > void;
}
type InternalNamePath = (string | number) [];class FormStore {
private store: Store = {}; // Store all the data in the form
private initialValues: Store = {}; // Form initial value, used when resetFields
private fieldEntities: FieldEntity[] = []; // Save all Field component instances in the form
private callbacks: Callbacks = {}; // The callback function
private setInitialValues = (initialValues: any, init: boolean) = > {
// Set the initial value
}
// =========================== Observer ===========================
private registerField = (entity: FieldEntity) = > {
// Register the Field component instance and trigger the fieldEntities pushdown
}
private dispatch = (action: ReducerAction) = > {
// When the Field component needs to modify the form value or trigger validation, it dispatches an action to inform FormStore to handle it
}
private notifyObservers = (
prevStore: Store, / / the original Store
namePathList: InternalNamePath[] | null.// A collection of paths to the changed form
) = > {
// Used to notify the Field component that the Store has changed and pass in the old Store and the new Store}}Copy the code
I believe you have a general idea of the FormStore, which controls all forms’ states and is the core hub of the form.
Form
Form does a few things
- Initialize the Form instance
- Register the callback functions onFieldsChange, onValuesChange, onFinish
- Set the initial value (Store only for the first rendering, initialValues only for other cases)
- Provide the Provider
- Render child node
const Form = ({ form, initialValues }: FormProps) = > {
// Initialize the Form instance
const [formInstance] = useForm(form);
const {
useSubscribe,
setInitialValues,
setCallbacks,
setValidateMessages,
setPreserve,
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);
// Configure the callback function
setCallbacks({
onValuesChange,
onFieldsChange: (changedFields: FieldData[], ... rest) = > {
formContext.triggerFormChange(name, changedFields);
if (onFieldsChange) {
onFieldsChange(changedFields, ...rest);
}
},
onFinish: (values: Store) = > {
formContext.triggerFormFinish(name, values);
if (onFinish) {
onFinish(values);
}
},
onFinishFailed,
});
// Flag bit, whether the first mount
const mountRef = React.useRef(null);
// Set the initial valuesetInitialValues(initialValues, ! mountRef.current);// Flag bit assignment
if(! mountRef.current) { mountRef.current =true;
}
// Initialize context value
const formContextValue = React.useMemo(
() = > ({
...(formInstance as InternalFormInstance),
validateTrigger,
}),
[formInstance, validateTrigger],
);
/ / the Provider
const wrapperNode = (
<FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider>
);
return (
<Component
{. restProps}
onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
event.stopPropagation();
formInstance.submit();
}}
>
{wrapperNode}
</Component>
);
}
Copy the code
Field
The Field component does several things
- Get the Form instance from the React Context
- Register yourself with FormStore
- Inject value and onChange into child components (value is retrieved from FormStore via Form instance)
- Render subcomponent
class Field extends React.Component {
public componentDidMount() {
const { shouldUpdate } = this.props;
// Get the getInternalHooks method
const { getInternalHooks }: InternalFormInstance = this.context;
const { registerField } = getInternalHooks(HOOK_MARK);
// Register yourself with FormStore
this.cancelRegisterFunc = registerField(this);
}
public onStoreChange() {
// When the FormStore value changes (onChange or setFieldsValue)
// FormStore calls this function (Field has already registered itself with FormStore)
// This is where the Field decides whether to update based on some criteria
}
public getControlled() {
const { getInternalHooks, getFieldsValue }: InternalFormInstance = this.context;
const{ dispatch } = getInternalHooks(HOOK_MARK); .// trigger defaults to onChange and can be changed using props
control[trigger] = (. args: EventArgs) = > {
// Mark as touched
this.touched = true;
this.dirty = true;
let newValue: StoreValue;
if(getValueFromEvent) { newValue = getValueFromEvent(... args); }else{ newValue = defaultGetValueFromEvent(valuePropName, ... args); }if (normalize) {
newValue = normalize(newValue, value, getFieldsValue(true));
}
// Notify FormStore of a value change
dispatch({
type: 'updateValue',
namePath,
value: newValue,
});
if(originTriggerFunc) { originTriggerFunc(... args); }}; .return control;
}
public render() {
const { resetCount } = this.state;
const { children } = this.props;
// What are the types of children
const { child, isFunction } = this.getOnlyChild(children);
let returnChildNode: React.ReactNode;
if (isFunction) {
returnChildNode = child;
} else if (React.isValidElement(child)) {
// Inject value and onChange through cloneElement
returnChildNode = React.cloneElement(
child as React.ReactElement,
this.getControlled((child as React.ReactElement).props),
);
} else{ warning(! child,'`children` of Field is not validate ReactElement.');
returnChildNode = child;
}
return <React.Fragment key={resetCount}>{returnChildNode}</React.Fragment>; }}Copy the code
conclusion
Rc-field-form uses the React Context to combine FormStore, Form, and field to create a state management library. The Field component connects to the state management library. It’s all transparent to the user.
This is the basic idea of RC-field-form. There are many details that are not covered in detail, such as how the FormList is implemented, how dependency verification works, how performance is guaranteed, and so on. Have the opportunity to write down and discuss with you.