1. Before the words

A component generator, as its name suggests, is one that generates components. Why you need a component generator is a matter of getting back to actual business scenario development.

As mentioned in the previous chapter of the React Common Solution series, business scenarios cannot be developed without business components, which are the secondary encapsulation of basic components and business data.

The implementation of secondary encapsulation can be component inheritance, default properties, higher-order components, and so on. The method of receiving a configuration that returns a rewrapped component is called the component generator.

Back to the problem of “why need component generator”, through the definition and implementation of “component generator”, in addition to creating a business with components and reusable business component, the component generator “can be a real business scenario” granularity component split “in the development of a ruler, let developers better organize business logic code.

In React development, everything is composed of components. Business scenarios are constructed through the combination of components. The granularity of components determines the coupling degree of components. The more logic the component has, the more complex the code 🤔, which gradually becomes a mountain of shit 😨, and the mountain becomes higher and higher 😂 in subsequent iterations.

The component generator, as a function, should be named for its purpose — generating XXX, so it can be named “buildXXX”, “createXXX”, “generateXXX”, “makeXXX”, etc. The author chose “buildXXX” as his personal preference.

The following is illustrated with actual business scenario code.

2. The body

A type of page commonly seen in business scenarios, form pages, the sample scenario code is as follows:

import React, { useCallback, useEffect, useState } from "react";

import { Form, Radio } from "@arco-design/web-react";

const Page: React.FC = () = > {
  / / option value
  const [options, setOptions] = useState<
    ArrayThe < {label: string; value: number} > > ([]);// Initializes the option value
  useEffect(() = >{ fetchOptions().then(setOptions); } []);// Submit form logic
  const handleSubmit = useCallback((formValue) = > {
    / / a little...} []);return (
    <div className="page">
      <Form onSubmit={handleSubmit}>
        <Form.Item field="a">
          <Radio.Group>
            <Radio value={1}>is</Radio>
            <Radio value={2}>no</Radio>
          </Radio.Group>
        </Form.Item>
        <Form.Item field="b">
          <Radio.Group>
            <Radio value={1}>is</Radio>
            <Radio value={2}>no</Radio>
          </Radio.Group>
        </Form.Item>
        <Form.Item field="c">
          <Radio.Group>
            {options.map((o) => (
              <Radio key={o.value} value={o.value}>
                {o.label}
              </Radio>
            ))}
          </Radio.Group>
        </Form.Item>
      </Form>
    </div>
  );
};

export default Page;
Copy the code

A quick look at the code shows that the form page above has three form fields, all of which use the same base component “Radio”.

The form page is simple and certainly meets the business requirements, but there is also obvious room for optimization as follows:

  • The “A” and “B” fields use duplicate components
  • The business data fetch logic of the component used by the “C” field is coupled to the form component

2.1 Components Removed

To solve the “duplication” and “coupling” problems, the first thing we do is “component decouple” them into “business components.”

2.1.1 Simple logic Extraction

The simple logical extraction results are as follows:

import { Radio } from "@arco-design/web-react";

// The A and B fields are business components
constRadio1: React.FC<{ value? :number; onChange? :(value? :number) = > void; } > =({ value, onChange }) = > {
  return (
    <Radio.Group value={value} onChange={onChange}>
      <Radio value={1}>is</Radio>
      <Radio value={2}>no</Radio>
    </Radio.Group>
  );
};

// "C" field business component
constRadio2: React.FC<{ value? :number; onChange? :(value? :number) = > void; } > =({ value, onChange }) = > {
  / / option value
  const [options, setOptions] = useState<
    ArrayThe < {label: string; value: number} > > ([]);// Initializes the option value
  useEffect(() = >{ fetchOptions().then(setOptions); } []);return (
    <Radio.Group value={value} onChange={onChange}>
      {options.map((o) => (
        <Radio key={o.value} value={o.value}>
          {o.label}
        </Radio>
      ))}
    </Radio.Group>
  );
};

Copy the code

But careful readers will notice that the “simply detached business component” still suffers from “duplicate type definitions” and loses some of the “native capabilities” of the “Radio” component by supporting only the “value” and “onChange” properties.

2.2.2 Default Attributes Are Removed

We apply the default properties to the React Common Solution and the result is as follows:

import { Radio, RadioGroupProps } from "@arco-design/web-react";

const Radio1: React.FC<RadioGroupProps> = ({
  options = [
    {
      label: "Yes",
      value: 1,
    },
    {
      label: "No." ",
      value: 2,},],... restProps }) = > {
  return <Radio.Group options={options} {. restProps} / >;
};

const Radio2: React.FC<RadioGroupProps> = (props) = > {
  / / option value
  const [options, setOptions] = useState<
    ArrayThe < {label: string; value: number} > > ([]);// Initializes the option value
  useEffect(() = >{ fetchOptions().then(setOptions); } []);return <Radio.Group options={options} {. props} / >;
};
Copy the code

After the above business component is removed, our form becomes simplified and the coupling degree between the business component and the form component is reduced. The code is as follows:

const Page: React.FC = () = > {
  / / a little...
  
  return (
    <div className="page">
      <Form onSubmit={handleSubmit}>
        <Form.Item field="a">
          <Radio1 />
        </Form.Item>
        <Form.Item field="b">
          <Radio1 />
        </Form.Item>
        <Form.Item field="c">
          <Radio2 />
        </Form.Item>
      </Form>
    </div>
  );
};
Copy the code

2.3 Component Generation

However, if we look closely at the Radio1 and Radio2 components, we can see that they “look very similar”. They are only different in the “business data”. Can we unify their “fetch logic” and configure the “data source” to generate them? Something like this:

const Radio1 = buildRadio(options1);

const Radio2 = buildRadio(options2);
Copy the code

This implementation is the Component builder, and its core formula is Component = buildComponent(options).

For higher-order components, WrappedComponent = withComponent(Component, options?) The ability to customize wrapped components over component Generator.

It is also important to note that component generators follow the requirements of secondary wrapping of components, that is, components generated from component generators have all the capabilities of the wrapped components, consistent with the idea of “higher-order components.”

The following explains how to write a component generator.

2.3.1 Unified Logic

Finding a unified logic for business components of the same type is the first step in “writing component generators”, and as mentioned earlier, the “fetch logic” of “Radio1” and “Radio2” can serve as their “unified logic”.

For the wrapped component radio.group, it receives an “options” property, so it uses the “result of the fetch logic” as the bottom value of its “options” property, as shown below:

<Radio.Group options={props.options ?? Result of fetch logic?? []} / >Copy the code

Radio1’s “data source” is “static/synchronous”, while Radio2’s “data source” is “dynamic/asynchronous”.

We use the compatibility principle when defining fetch logic, dynamic/asynchronous is compatible with static/synchronous.

Therefore, we define a configuration parameter named “getOptions” corresponding to the “fetch logic”, which is defined as follows:

type IRadioOption = { label: React.ReactNode; value: number | string };

type getOptions = () = > Promise<Array<IRadioOption>>;
Copy the code

2.3.2 Generator configuration

With the configuration parameters summarized above, we can then define the “component generator” function, called “buildRadio”, as follows:

type IRadioOption = { label: React.ReactNode; value: number | string };

type IBuildRadioOptions = {
  /** fetch logic */
  getOptions: () = > Promise<Array<IRadioOption>>;
};


export function buildRadio(options: IBuildRadioOptions) {
  // TODO generator implementation
}
Copy the code

2.3.3 Generator implementation

With a clear definition of the generator, and following the requirements for secondary packaging of components, our generator implementation is as follows:

type IRadioOption = { label: React.ReactNode; value: number | string };

type IBuildRadioOptions = {
  /** fetch logic */
  getOptions: () = > Promise<Array<IRadioOption>>;
};

export function buildRadio(options: IBuildRadioOptions) {
  const { getOptions } = options;

  const RadioBase = ({ options: propsOptions, ... restProps }: RadioGroupProps, ref: ForwardedRef<any>
  ) = > {
    / / option value
    const [options, setOptions] = useState<Array<IRadioOption>>([]);

    // Initializes the option value
    useEffect(() = >{ getOptions().then(setOptions); } []);return (
      <Radio.Group
        {. restProps}
        ref={ref}
        options={propsOptions ?? options?? []} / >
    );
  };

  return forwardRef<any, RadioGroupProps>(RadioBase);
}
Copy the code

Through the “component generator” implemented above, “Radio1” and “Radio2” components can be generated through the “component generator”, the sample code is as follows:

const Radio1 = buildRadio({
  getOptions: () = >
    Promise.resolve([
      {
        label: "Yes".value: 1}, {label: "No." ".value: 2,})});const Radio2 = buildRadio({
  getOptions: fetchOptions,
});
Copy the code

3. Advanced

Here, many readers may feel that this “component generator” is not “component secondary wrapper function”, and create a new term, quite bluffing 😆.

The strength of the component generator lies in how we implement it around its core formula and give it sufficient configuration capabilities. Next I will update the configuration capabilities of the “component Generator” above.

Here I post another form linkage scenario, with the following example scenario code:

const Page: React.FC = () = > {
  / / option value
  const [options, setOptions] = useState<
    ArrayThe < {label: string; value: number} > > ([]);// Submit form logic
  const handleSubmit = useCallback((formValue) = > {
    / / a little...} []);return (
    <div className="page">
      <Form onSubmit={handleSubmit}>
        <Form.Item field="d">
          <Radio1 onChange={(v)= > fetchOptions(v).then(setOptions)} />
        </Form.Item>
        <Form.Item field="e">
          <Radio.Group options={options} />
        </Form.Item>
      </Form>
    </div>
  );
};
Copy the code

Through the code analysis of the above scenario, it can be seen that there are two form fields “D” and “E”, among which the change of form value of form field “D” triggers the request for the option value of form field “E”. That is, the business component of the form field “E” needs to support “dependency updates.”

Here is no more elaboration, directly to the “component generator” advanced ability upgrade, after the upgrade code implementation is as follows:

type IRadioOption = { label: React.ReactNode; value: number | string };

type IBuildRadioOptions<P> = {
  /** Depends on attributes */deps? :Array<string>;
  /** fetch logic */
  getOptions: (props: P) = > Promise<Array<IRadioOption>>;
};

export function buildRadio<P = {}>(options: IBuildRadioOptions<P>) {
  const { deps = [], getOptions } = options;

  const RadioBase = (props: P & RadioGroupProps, ref: ForwardedRef<any>) = > {
    const { options: propsOptions, ... restProps } = props;/ / option value
    const [options, setOptions] = useState<Array<IRadioOption>>([]);

    // Listen for dependent properties to update option values
    useEffect(
      () = > {
        getOptions(props).then(setOptions);
      },
      deps.map((name) = > (props as Record<string, unknown>)[name])
    );

    return (
      <Radio.Group
        {. omit(restProps.deps)}
        ref={ref}
        options={propsOptions ?? options?? []} / >
    );
  };

  return forwardRef<any, P & RadioGroupProps>(RadioBase);
}
Copy the code

Therefore, through the “Advanced component Generator”, we can extract the business components of the form field “E” with the following code:

const Radio3 = buildRadio<{
  d: number; ({} >relation: ["d"].getOptions: ({ d }) = > fetchOptions(d),
});
Copy the code

The optimized scenario code results as follows:

const Page: React.FC = () = > {
  // List of options
  const [form] = Form.useForm();

  // Submit form logic
  const handleSubmit = useCallback((formValue) = > {
    / / a little...} []);return (
    <div className="page">
      <Form onSubmit={handleSubmit} form={form}>
        <Form.Item field="d">
          <Radio1 />
        </Form.Item>
        <Form.Item field="e" shouldUpdate={true}>
          <Radio3 d={form.getFieldValue("d")} / >
        </Form.Item>
      </Form>
    </div>
  );
};
Copy the code

4. Develop

4.1 buildCheckbox

The base component “Checkbox” is similar to “Radio”, so its “component generator” is similar as follows:

type ICheckboxValue = number | string;

type ICheckboxOption = { label: React.ReactNode; value: ICheckboxValue };

type IBuildCheckboxOptions<P> = {
  /** Depends on attributes */deps? :Array<string>;
  /** fetch logic */
  getOptions: (props: P) = > Promise<Array<ICheckboxOption>>;
};

export function buildCheckbox<P = {}>(options: IBuildCheckboxOptions<P>) {
  const { deps = [], getOptions } = options;

  const CheckboxBase = (
    props: P & CheckboxGroupProps<ICheckboxValue>,
    ref: ForwardedRef<any>
  ) = > {
    const { options: propsOptions, ... restProps } = props;/ / option value
    const [options, setOptions] = useState<Array<ICheckboxOption>>([]);

    // Listen for dependent properties to update option values
    useEffect(
      () = > {
        getOptions(props).then(setOptions);
      },
      deps.map((name) = > (props as Record<string, unknown>)[name])
    );

    return (
      <Checkbox.Group
        {. omit(restProps.deps)}
        ref={ref}
        options={propsOptions ?? options?? []} / >
    );
  };

  return forwardRef<any, P & CheckboxGroupProps<ICheckboxValue>>(CheckboxBase);
}
Copy the code

The usage is consistent with “buildRadio”, which will not be demonstrated here.

4.2 buildSwitch

The basic component “Switch” is special. As a Switch, it can only have two values, either “on” or “off”. In actual business scenarios, its value is generally fixed, so we generally only need to consider the “static/synchronous” “fetch logic”, which is implemented as follows:


type IBuildSwitchOptions<T> = {
  /** Value mapping configuration */
  valueMap: {
    /** Enable the value */
    on: T;
    /** Close the value */
    off: T;
  };
};

type ISwitchProps<T> = Omit<SwitchProps, "onChange"> & {
  /** Component value */value? : T;/** value change callback function */onChange? :(value: T) = > void;
};

export function buildSwitch<T = boolean> (options: IBuildSwitchOptions<T>) {
  const { valueMap } = options;

  function SwitchBase(props: ISwitchProps<T>, ref: ForwardedRef<any>) {
    const{ value, onChange, ... restProps } = props;return (
      <Switch
        {. restProps}
        ref={ref}
        checked={value= = =valueMap.on}
        onChange={(v)= >{ onChange? .(v ? valueMap.on : valueMap.off); }} / >
    );
  }

  return forwardRef<any, ISwitchProps<T>>(SwitchBase);
}
Copy the code

Example code for “buildSwitch” is as follows:

const DemoSwitch = buildSwitch({
  valueMap: {
    on: "Hello".off: "World",}});/ / a little...

return (
  <>
    <DemoSwitch />
  </>
);
Copy the code

5. The last

“Component generator” is a kind of method that readers often use in practical development. The programming idea of “build” also guides the author to write every component well. If you can read this, I also hope you can have some insights and harvest, and I hope to bring help to you in practical development.

React Common Solution is a summary of my work in the past three years. I will write this article as soon as I think of it. In the next article, I will write “React Common Solution — Form Container”

The last byte has a large number of posts put out (school recruitment agency recruitment all have, push links in the author’s personal introduction), look forward to interested leaders enthusiastically deliver ha, I also incidentally earn some push bonus (red envelope is also a few cents).