Reading other people’s code is an important part of work and learning. This series is a record of my mental process when I read some project code. Limited by personal ability and experience, there must be a variety of deficiencies in the article, please give me more advice.

As the first in the series, the victim in this article is a React component named RC-field-form. Some of you may not be familiar with the name, but most of you certainly have, as it is the component used at the bottom of the well-known ANTD. Before we start, let’s go over how it works because I’ve forgotten it too.

How does it work

First, clone the project locally and head straight to the docs/examples directory. There are many examples of components in use. Let’s start with the simplest one:

// examples/basic.tsx

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

const list = new Array(1111).fill(() = > null);

interfaceFormValues { username? :string; password? :string; path1? : { path2? :string;
  };
}

export default class Demo extends React.Component {
  formRef: any = React.createRef<FormInstance<FormValues>>();

  onFinish = (values: FormValues) = > {
    console.log("Submit:", values);

    setTimeout(() = > {
      this.formRef.current.setFieldsValue({ path1: { path2: "2333"}}); },500);
  };

  public render() {
    return( <div> <h3>State Form ({list.length} inputs)</h3> <Form<FormValues> ref={this.formRef} onFinish={this.onFinish}> <Field  name="username"> <Input placeholder="Username" /> </Field> <Field name="password"> <Input placeholder="Password" /> </Field> <Field name="username"> <Input placeholder="Shadow of Username" /> </Field> <Field name={["path1", "path2"]}> <Input placeholder="nest" /> </Field> <Field name={["renderProps"]}> {(control) => ( <div> I am render props <Input {... control} placeholder="render props" /> </div> )} </Field> <button type="submit">Submit</button> <h4>Show additional field when `username` is `111`</h4> <Field<FormValues> dependencies={["username"]}> {(control, meta, context) => { const { username } = context.getFieldsValue(true); console.log("my render!" , username); return ( username === "111" && ( <Input {... control} placeholder="I am secret!" / >)); }} </Field> {list.map((_, index) => ( <Field key={index} name={`field_${index}`}> <Input placeholder={`field_${index}`} /> </Field> ))} </Form> </div> ); }}Copy the code

Reading this code, I found the following: The library exports two important components: forms and fields. It appears that the Form creates an object of type FormInstance to which the logical processing of the Form should be delegated. Form and Field need to be used together, they are not isolated. Field can read the value of the Form item from the formInstance of the Form. As you can see, the React Context is not used to pass formInstasnce to the Field directly.

How do I read it

Formcomponent

Look at the Form code and find the following code:

const [formInstance] = useForm(form);
Copy the code

My intuition tells me that useForm is the most critical code and that we should start with the useForm.ts code.

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 = newFormStore(forceReRender); formRef.current = formStore.getForm(); }}return [formRef.current];
}
Copy the code

UseForm is of type (form? : FormInstance

) => [FormInstance

], so I reasonably suspect that this parameter is used to reuse the existing FormInstance, This may be done to preserve internal state for formInstance or to keep state synchronization between two forms. Next, a Ref is created to hold a formInstance, and the if statement ensures that it is assigned only once. Here’s the first tip I’ll put on my notebook today:

// forceUpdate implementation of functional components
const [, forceUpdate] = React.useState({});

const forceReRender = () = > {
  forceUpdate({});
};
Copy the code

Next, we find that formInstance is provided by the FormStore object created through new FormStore. Because FormStore code is a little long, I’m not going to post it here. Its constructor simply saves the passed forceRootUpdate and does nothing else.

public getForm = (): InternalFormInstance= > ({
    getFieldValue: this.getFieldValue,
    getFieldsValue: this.getFieldsValue,
    getFieldError: this.getFieldError,
    getFieldWarning: this.getFieldWarning,
    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,
});
Copy the code

GetForm returns an object made up of FormStore’s partial methods. I guess this prevents users from accessing FormStore’s private fields and methods directly.

Coming back to the form.tsx code,

const {
  useSubscribe,
  setInitialValues,
  setCallbacks,
  setValidateMessages,
  setPreserve,
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);
Copy the code

As you can see, this is transforming formInstance into InternalFormInstance. UseForm uses FormInstance because useForm is to be exported for users to use, while FormInstance only has some public methods. Instead of FormInstance, the type of validateFields for InternalFormInstance is replaced with an internal version, and several methods are added. Next, get some internal methods via getInternalHooks. One more comment about getInternalHooks:

/**
  * Form component should register some content into store.
  * We pass the `HOOK_MARK` as key to avoid user call the function.
  */
Copy the code

It seems that the author has gone to great lengths to prevent users from calling this method. Continue to:

React.useEffect(() = > {
  formContext.registerForm(name, formInstance);
  return () = > {
    formContext.unregisterForm(name);
  };
}, [formContext, formInstance, name]);
Copy the code

Where formContext was created earlier with React. UseContext (formContext). TSX does export the FormProvider component, but it is not used in the basic example we just saw. That means that we’re using the default value for FormContext. However, the default method is noop, no operation. Now, let’s just forget about the formContext code.

setValidateMessages({ ... formContext.validateMessages, ... validateMessages, });Copy the code

The validateMessages are from props.

Prop Description Type Default
validateMessages Set validate message template ValidateMessages

You can conclude that this is done with a hint from the validate definition, which should also be useful for internationalization. Continue to:

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,
});
Copy the code

This code basically hooks the Callbacks in props to the formStore so that the formStore can trigger incoming callbacks at the appropriate time.

setPreserve(preserve);
Copy the code
Prop Description Type Default
preserve Preserve value when field removed boolean false

This is used to configure whether the value of the binding should be retained when the field is unmounted. Next, it’s time to take notes:

// Set initial value, init store value when first mount
const mountRef = React.useRef(null); setInitialValues(initialValues, ! mountRef.current);if(! mountRef.current) { mountRef.current =true;
}
Copy the code

What this code does, as this comment explains quite clearly, is sort of like the life cycle function componentDidMount.

// Prepare children by `children` type
let childrenNode = children;
const childrenRenderProps = typeof children === "function";
if (childrenRenderProps) {
  const values = formInstance.getFieldsValue(true);
  childrenNode = (children as RenderProps)(values, formInstance);
}

// Not use subscribe when using render propsuseSubscribe(! childrenRenderProps);Copy the code

For the children. If children is used as a Render prop, pass in all form values along with formInstance. Also, don’t be fooled by the name useSubscribe; it’s just a method used to configure formStore. The author wants to set the formStore subscribable to true only when Render Prop is not used. I’ll leave that to what exactly subscribable controls the behavior of the formState. Next, I skipped over the code for working with Redux and came to:

const formContextValue = React.useMemo(
  () = > ({
    ...(formInstance as InternalFormInstance),
    validateTrigger,
  }),
  [formInstance, validateTrigger]
);

const wrapperNode = (
  <FieldContext.Provider value={formContextValue}>
    {childrenNode}
  </FieldContext.Provider>
);
Copy the code

The fieldContext. Provider is used to wrap children in the Field component to form a formInstance. This also confirms what we already knew about the React Context. Between you and me, I think the React hooks design is a bit outclassed by Vue 3’s Composition API. Because Hooks need to think carefully about deps when using them. For some developers who are new to using hooks, this feels error-prone.

To summarize, all the Form component does is create and configure the formStore and inject the FieldContext.

Fieldcomponent

The default export of field. TSX is the WrapperField component. In this component, we get the FieldContext via react. useContext(FieldContext) and pass it to the Field component. Field is a class component, and the code is a bit long, so we’ll try to focus on it.

The first is constructor. Constructor calls the initEntityValue method of formStore and passes this. In initEntityValue, get the initialValues passed to the Field component. If the corresponding item in the formStore has not already been initialized, use this initialValues. Otherwise, do nothing. There is a minor detail here, because initEntityValue is called from constructor, so the formStore can still fetch the key value of the Field even if the component is not mounted.

Next, I decided to look at Render first. Render is the first place where you might need to take notes. When writing components, dealing with children can be a bit of a headache. Children can accept values of type ReactNode.

type ReactNode =
  | ReactChild
  | ReactFragment
  | ReactPortal
  | boolean
  | null
  | undefined;

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
Copy the code

The possible types of ReactNode are too varied. However, there are times when we want to limit a user’s children choices. Here, for example, the author wants the user to pass in only one React element. But passing in an array or Fragment that contains only one React Element should also be correct. Let’s see how the author deals with the situation:

const childList = toChildrenArray(children);
if(childList.length ! = =1| |! React.isValidElement(childList[0]) {// ...
} else {
  // ...
}
Copy the code

The toChildrenArray here is a helper function from the RC-util package. It normalizes the complex children into a flat array:

function toArray(children: React.ReactNode) :React.ReactElement[] {
  let ret: React.ReactElement[] = [];

  React.Children.forEach(children, (child: any) = > {
    if (child === undefined || child === null) {
      return;
    }

    if (Array.isArray(child)) {
      ret = ret.concat(toArray(child));
    } else if (isFragment(child) && child.props) {
      ret = ret.concat(toArray(child.props.children));
    } else{ ret.push(child); }});return ret;
}
Copy the code

Once you get the array, take the first element of the array as the only recognized child. Finally, clone the child and pass in control. That is, in the following code,

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

The Input component takes the values from The formStore and notifies the formStore of the cause when an event occurs. Of course, from the example below, you can see that Field’s children support is used as a Render prop, and that Field passes control and meta information directly (there is no need to clone the returned Element at this point).

<Field name={["renderProps"]} > {(control) = > (
    <div>
      I am render props
      <Input {. control} placeholder="render props" />
    </div>
  )}
</Field>
Copy the code

I don’t know what control is, but I can probably guess that it looks something like this:

type Control<T> = {
  value: T;
  onChange: (x: T) = > void;
};
Copy the code

So let’s see what control is. Control is returned by getControlled and contains important code such as:

const value = this.getValue();
const getValueProps = (val: StoreValue) = > ({ [valuePropName]: val });

constcontrol = { ... mergedGetValueProps(value), };// Add trigger
// trigger refers to the time when a value is collected
control[trigger] = (. args: EventArgs) = > {
  // Mark as touched
  this.touched = true;
  this.dirty = true; // The difference between dirty and touched is that dirty will also be set to true when validate is set

  this.triggerMetaEvent(); // Triggers the meta update event

  letnewValue: StoreValue = getValueFromEvent(... args);// Get a new value

  dispatch({
    type: "updateValue",
    namePath,
    value: newValue,
  }); / / update the formStore
};

// Add validateTrigger
const validateTriggerList: string[] = toArray(mergedValidateTrigger || []);

validateTriggerList.forEach((triggerName: string) = > {
  // Wrap additional function of component, so that we can get latest value from store
  const originTrigger = control[triggerName];
  control[triggerName] = (. args: EventArgs) = > {
    if (originTrigger) {
      // Trigger timing is also a validteTrigger timing handleroriginTrigger(... args); }// Always use latest rules
    const { rules } = this.props;
    if (rules && rules.length) {
      // We dispatch validate to root,
      // since it will update related data with other field with same name
      dispatch({
        type: "validateField", namePath, triggerName, }); }}; });Copy the code

I added as many comments as I could. Why isn’t it explained line by line? Because I’m a little tired. In short, control is really a function that contains form values and some event handling. FormStore has two events that we need to focus on: updateValue and validateField.

In the case of Meta, this is some information about the current state of the Field. It is important to note that this information is not placed in the state of the Field, but in the property of the class. The problem then is that we know that changing state with setState triggers component updates, whereas reassigning a normal property does not. Again, the collection and processing of form values is actually left to formStore. It is therefore reasonable to suspect that updates to the Field component are also triggered by an external formStore. Keep an eye out for this when you read formState’s updateValue and validateField event handlers.

Later, in componentDidMount, the component instance is registered with formStore by calling formStore’s registerField.

this.fieldEntities.push(entity);

// Set initial values
if(entity.props.initialValue ! = =undefined) {
  const prevStore = this.store;
  this.resetWithFieldInitialValue({ entities: [entity], skipExist: true });
  this.notifyObservers(prevStore, [entity.getNamePath()], {
    type: "valueUpdate".source: "internal"}); }Copy the code

RegisterField pushes Field instances into fieldEntities. If initialValue is set, reset the corresponding value in formStore to its initialValue, and then call notifyObservers that a value has been updated for the Field instance. ResetWithFieldInitialValue code is a bit long, here also won’t stick. In general, its function is to reset some or all of the fields to their initial values. But there are a few caveats:

  1. If the Form already specifies an initial value for a key, the Field whose name is the key cannot specify an initial value again.
  2. If multiple fields correspond to the same key, there can be only one specified initial value.

This description may be a little confusing, but just remember that the initial value specification does not conflict. And registerField invokes the resetWithFieldInitialValue skipExist travels is true, that is to say, if you have corresponding key value in formStore, there is no need to reset. But skipExist usually passes false when this function is called unsolicited elsewhere, otherwise it doesn’t make sense.

Remember that in Field constructor, values have been set? Unless the value is null again after the constructor call and before componentDidMount is fired, the formStore key will not be reset when the registerField function is called.

NotifyObservers should be the focus.

notifyObservers = (
  prevStore: Store,
  namePathList: InternalNamePath[] | null,
  info: NotifyInfo
) = > {
  if (this.subscribable) {
    constmergedInfo: ValuedNotifyInfo = { ... info,store: this.getFieldsValue(true),};this.getFieldEntities().forEach(({ onStoreChange }) = > {
      onStoreChange(prevStore, namePathList, mergedInfo);
    });
  } else {
    this.forceRootUpdate(); }};Copy the code

Subscribable we’ve already met before. When children of the Form is used as render prop, subscribable is set to false. By refreshing the Form, the latest status is passed to the function as children, and formStore does not need to worry about how to update the function. Otherwise, the onStoreChange method of the specified fields is called. In onStoreChange, the Field decides how to update its state and whether it needs to be rerendered. My previous confusion about how to update the Field was solved.

Finally, let’s look at formStore’s handling of updateValue and validateField events. The updateValue method handles the updateValue event by updating the store and notifying the corresponding fields. Look for any fields that are dependent on the value of this key. If so, trigger a validate process for these affected fields, and notify them through notifyObservers that the dependency has changed. Again, these fields will determine in onStoreChange whether the state needs to be updated and re-rendered, etc. Finally, some callback functions are triggered.

The validateFields method handles the validateFields event. The method finds the target fields and then calls their validateRules method, collecting the return values to form an array of promises. Wait until all promises finish, notify the affected fields, and then trigger the relevant callback functions previously registered. Note also that because this process is asynchronous, a validate event may have already been emitted by the time the Promise goes through the FINISHED state, so the author handles this as well, returning an error message along with information about whether the error is obsolete. ValidateRules call validateRules in utils/ ValidteUtils. ts, and finally use Async-Validator to complete validation.