preface

There are a lot of React form components on the market, and they all work well. Recently, I decided to develop a small UI library. It would be very complicated to write forms by myself, so I chose Ant Disign to implement forms

As we all know, most Ant Design components are implemented based on the React-Component, and the Form component is implemented based on the RC-Field-Form, with some additional functionality. If you can understand the logic of the RC-field-form, how does the Form component work in Ant Design

<Form>
  <Field name="username">
    <Input placeholder="Username" />
  </Field>
  <Field name="password">
    <Input placeholder="Password" />
  </Field>
</Form>
Copy the code

Basic logic

The implementation of field-form is still based on the Context. If the Context is used in conjunction with Hooks, there is a big problem: it will refresh completely. If a value changes, it will refresh even if the component does not use the value.

Form-field itself stores form values and updates components, using only Context to pass methods

// useForm.ts

// Create a FormStore instance to process forms
const formStore: FormStore = new FormStore(forceReRender);
// Call the getForm() method to get the method, lest some of the internal properties be accessed
formRef.current = formStore.getForm();
Copy the code
// form.tsx // useForm to get formref.current, pass the method to context, Each Field Field component gets these methods < fieldContext. Provider value={formContextValue}>{childrenNode}</ fieldContext.provider >Copy the code

Register the Field form element

FormStore is an instance that processes forms, including the value Store of the entire form, and the methods that update the Store. FormStore is a separate class from React. The Field component registers the methods that refresh the component into FormStore

// Field.tsx

public componentDidMount() {
    const { shouldUpdate } = this.props;
    // Use context to get the internal method of the FormStore instance
    const { getInternalHooks }: InternalFormInstance = this.context;
    const { registerField } = getInternalHooks(HOOK_MARK);
    // Register the current Field component instance to the Form
    this.cancelRegisterFunc = registerField(this);
}
Copy the code

The Field component is a Class, not an Hooks component. The reason is that Hooks need to write too much extra code. The Class can easily register the entire Field component with FormStore, so that the methods of the Field component can be called from FormStore

The change of the value

The value change process is similar to Redux. When the value of the Field component changes, a Dispatch is sent, and the FormStore changes the value after receiving it, notifies all the registered Field components to compare the ratio. If updates are required, call this.forceUpdate() in React to force the component to refresh

First, you pass the value and onChange to the form element component

// Field.tsx

// Create the onChange function and the 'value' field
public getControlled = (childProps: ChildProps = {}) = > {
    // Some of the code has been deleted for easy reading

    // The current namePath
    const { getInternalHooks }: InternalFormInstance = this.context;
    const { dispatch } = getInternalHooks(HOOK_MARK);
    const value = this.getValue();

    // The original trigger function on the children form element component
    // If children already have onChange, it will still be called if it is controlled by Field
    const originTriggerFunc: any = childProps.onChange;

    // This is the props to pass to children
    constcontrol = { ... childProps, value, };// Function to change the value, default is onChange
    control.onChange = (. args: EventArgs) = > {
        // The default value is event.target.value
        letnewValue: StoreValue = defaultGetValueFromEvent(valuePropName, ... args);// ...
        
        // Dispatches dispatches, and FormStore updates the value of store
        dispatch({
            type: 'updateValue',
            namePath,
            value: newValue,
        });

        // Call the children function
        if (originTriggerFunc) {
            originTriggerFunc(...args);
        }
    };
	// ...
    
    return control;
};

Copy the code

The getControlled() method creates these two fields, notifies the Store of the change, and calls the original onChange on the component, passing the original props of the component along

Pass these two fields to the controlled form element component at render time

Public render() {const {children} = this.props; let returnChildNode: ReactNode = react. cloneElement(children as react. ReactElement, // child-props) This.getcontrolled ((children as react.reactelement).props),); return <React.Fragment>{returnChildNode}</React.Fragment>; }Copy the code

Notify the Field component to refresh

You also need to notify all registered Field components of updates when the FormStore form values change

// useForm.tsx   

// class FormStore
// The Field component updates the value by calling Dispatch
private dispatch = (action: ReducerAction) = > {
  switch (action.type) {
      case 'updateValue': {
          const { namePath, value } = action;
          / / update the value
          this.updateValue(namePath, value);
          break;
      }
      // ...
};
    
/ / update the value
private updateValue = (name: NamePath, value: StoreValue) = > {
    const namePath = getNamePath(name);
    const prevStore = this.store;
    // Make a deep copy of the value
    this.store = setValue(this.store, namePath, value);

    // Notifies registered Field components of updates
    this.notifyObservers(prevStore, [namePath], {
        type: 'valueUpdate',
        source: 'internal'});// ...
};
    
// Notify the observer, that is, notify all Field components
private notifyObservers = (
    prevStore: Store,
    namePathList: InternalNamePath[] | null,
    info: NotifyInfo,
) => {
    // The current store value and NotifyInfo
    constmergedInfo: ValuedNotifyInfo = { ... info, store:this.getFieldsValue(true),};// Get all field instances and call onStoreChange
    // This is the React instance of the Field component
    this.getFieldEntities().forEach(({ onStoreChange }) = > {
        onStoreChange(prevStore, namePathList, mergedInfo);
    });
};
Copy the code

Once the Field component changes, dispatch dispatches to change the FormStore value, and call notifyObservers internally to notify each Field component of parameters passing

So what should I do if the value changes or does it go back to the Field component

// Field.tsx

public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) = > {
  const { shouldUpdate } = this.props;
  const { store } = info;
  const namePath = this.getNamePath();
  // The last value of the current field
  const prevValue = this.getValue(prevStore);
  // The current field value
  const curValue = this.getValue(store);

  // Whether the current field NamePath is in namePathList
  const namePathMatch = namePathList && containsNamePath(namePathList, namePath);

  switch (info.type) {
	// ...
        
    default:
      // Refresh the component if the value changes
      if (
        namePathMatch ||
        dependencies.some(dependency= >
          containsNamePath(namePathList, getNamePath(dependency)),
        ) ||
        requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info)
      ) {
        this.reRender();
        return;
      }
      break;
  }
// ...
};


Copy the code

Just the basic logic of how the values change that’s it, or is it convoluted

conclusion

The biggest drawback for me is that I don’t provide useField. In the age of hooks, I still use render props, which is a bit strange. Unless you expose store changed subscription events, this should be easier to do


I can’t do simple analysis. I have a big head and have been watching it intermittently for a month. Today, I want to summarize the results

Sure enough, ran to see the source code and see the source code is not too useful, or have to do the same function to see the source code reference implementation more reliable