laitimes

How to develop components that everyone loves?

author:Technical Alliance Forum

Wang Yinye (Feng Shui) Ali Developer 2023-05-22 09:01 Posted in Zhejiang

How to develop components that everyone loves?

Ali Mei's guide

This article is similar to a recipe, comparing the content of some component designs, and the author distinguishes its importance according to 1~5 stars. (Reply to big data in the background to get the "Big Data & AI Practical School" e-book)

Components are the most common thing that the front end deals with, and for applications such as React and Vue, everything is a component.

Some students with work experience know that components are actually hierarchical, some components can be reused by tens of thousands of developers, and some components can only run in the project, or even move to another project of their own.

How to examine the level of a front-end, you can first see whether he has provided reusable components to the team, if a front-end has only been able to use what he writes, or has never provided reusable technology externally, then his contribution to a team must be limited.

So what should I consider when I start writing a component that can be opened?

This article is similar to a recipe, comparing the fragmentary recording of some component design content, I distinguish its importance according to 1~5 ⭐️.

conscious

First of all, at the level of consciousness, we need to observe this component from the perspective of the developer who uses the component, so the following points need to be planted in consciousness during the component development process:

1. I should pay attention to the TypeScript API definition, and a good component API should look natural and never redundant.

2. I should focus on README and Mock, a component without documentation = no, it is better not to use the link mode to develop components.

3. I should not introduce any side-effect dependencies, such as global state (Vuex, Redux), unless they can self-converge.

4. I'm working on an open component, and it's likely that someone will come to see my code in the future, and I have to write it well.

Interface design

A good interface is the fastest way for developers to figure out the parameters of components, and it is also the premise for your subsequent development to have better code hints.

type Size = any; //  ❌
type Size = string; // ♀️
type Size = "small" | "medium" | "large"; // ✅           

DOM Properties(⭐️⭐️⭐️⭐️⭐️)

Components eventually need to become the page DOM, so if your component isn't one-off, define the underlying DOM property types by default. className can be handled using classnames[1] or clsx[2], don't handle className manually!

export interface IProps {
className?: string;
  style?: React.CSSProperties;
}           

For internal business, there will also be DOM attributes such as data-spm, which are mainly used to bury the reported content, so you can directly make a basic encapsulation of your Props type:

export type CommonDomProps = {
className?: string;
  style?: React.CSSProperties;
} & Record<`data-${string}`, string>
// component.tsx
export interface IComponentProps extends CommonDomProps {
  ...
}
// or
export type IComponentProps = CommonDomProps & {
  ...
}           

Type Annotation() ⭐️⭐️⭐️

1. export component props type definition

2. Add a comment for the specification for the type exposed by the component

export type IListProps{
/**
   * Custom suffix element.
   * Used to append element after list
   */
  suffix?: React.ReactNode;
/**
   * List column definition.
   * This makes List acts like a Table, header depends on this property
   * @default []
   */
  columns?: IColumn[];
/**
   * List dataSource.
   * Used with renderRow
   * @default []
   */
  dataSource?: Array<Record<string, any>>;
}           

The above type comment is a canonical type comment, and the clear type comment allows consumers to click directly into your type definition to see a clear explanation of this parameter.

At the same time, such type annotations that conform to the jsdoc[3] specification are also a standard community specification. USING COMPONENT DEMOS LIKE VITDOC[4] CAN ALSO HELP YOU QUICKLY GENERATE BEAUTIFUL API DOCUMENTATION.

Tip: If you're tired of writing these comments, try the famous AI code plugin: Copilot[5], which can help you quickly generate the text you want to express.

❌ Here is a demonstration of the error:

toolbar?: React.ReactNode; // List toolbar.
//  Columns 
// defaultValue is "[]"
  columns?: IColumns[];           

Component Slot() ⭐️⭐️⭐️

For a novice component developer, it is often the mistake of replacing ReactNode with a string type.

For example, to define a label props for an Input component, many novice developers will use string as the label type, but this is wrong.

export type IInputProps = {
  label?: string; // ❌
}
export type IInputProps = {
  label?: React.ReactNode; // ✅
}           

When encountering this type, you need to realize that we are actually providing a React slot type, and if you just let it be displayed in the component consumption and do nothing else, you should use the ReactNode type as the type definition.

How to develop components that everyone loves?

Controlled vs. Uncontrolled(⭐️⭐️⭐️⭐️⭐️)

If the type of component to be encapsulated is for the purpose of data entry, that is, there are components that have two-way bindings. Be sure to provide the following type definitions:

export type IFormProps<T = string> = {
  value?: T;
  defaultValue?: T;
  onChange?: (value: T, ...args) => void;
};           

Also, such interface definitions are not necessarily for value, but are required for all components with controlled requirements, such as:

export type IVisibleProps = {
/**
   * The visible state of the component.
   * If you want to control the visible state of the component, you can use this property.
   * @default false
   */
  visible?: boolean;
/**
   * The default visible state of the component.
   * If you want to set the default visible state of the component, you can use this property.
   * The component will be controlled by the visible property if it is set.
   * @default false
   */
  defaultVisible?: boolean;
/**
   * Callback when the visible state of the component changes.
   */
  onVisibleChange?: (visible: boolean, ...args) => void;
};           

For specific reasons, please see: Controlled Components and Uncontrolled Components[6]

Recommended consumption method: ahooks useControllableValue[7]

Form Class Common Properties (⭐️⭐️⭐️⭐️)

If you are packaging a form-type component that may be consumed in the future with Form components such as antd[8]/fusion[9], you may need to define the following types:

export type IFormProps = {
/**
   * Field name
   */
  name?: string;
/**
   * Field label
   */
  label?: ReactNode;
/**
   * The status of the field
   */
  state?: 'loading' | 'success' | 'error' | 'warning';
/**
   * Whether the field is disabled
   * @default false
   */
  disabled?: boolean;
/**
   * Size of the field
   */
  size?: 'small' | 'medium' | 'large';
/**
   * The min value of the field
   */
  min?: number;
/**
   * The max value of the field
   */
  max?: number;
};           

Select Type(⭐️⭐️⭐️⭐️)

If you are developing a component that needs to be selected, you may use the following types:

export interface ISelection<T extends object = Record<string, any>> {
/**
   * The mode of selection
   * @default 'multiple'
   */
  mode?: 'single' | 'multiple';
/**
   * The selected keys
   */
  selectedRowKeys?: string[];
/**
   * The default selected keys
   */
  defaultSelectedRowKeys?: string[];
/**
   * Max count of selected keys
   */
  maxSelection?: number;
/**
   * Whether take a snapshot of the selected records
   * If true, the selected records will be stored in the state
   */
  keepSelected?: boolean;
/**
   * You can get the selected records by this function
   */
  getProps?: (record: T, index: number) => { disabled?: boolean; [key: string]: any };
/**
   * The callback when the selected keys changed
   */
  onChange?: (selectedRowKeys: string[], records?: Array<T>, ...args: any[]) => void;
/**
   * The callback when the selected records changed
   * The difference between `onChange` is that this function will return the single record
   */
  onSelect?: (selected: boolean, record: T, records: Array<T>, ...args: any[]) => void;
/**
   * The callback when the selected all records
   */
  onSelectAll?: (selected: boolean, keys: string[], records: Array<T>, ...args: any[]) => void;
}           

The above parameter definitions can be viewed and consumed by referring to Merlion UI - useSelection[10]. 

In addition, when single selection and multiple selection exist, the value of the component may need to automatically change the data type according to the mode transmitted down.

For example, in the Select component, there are the following differences:

mode="single" -> value: string | number
mode="multiple" -> value: string[] | number[]           

So for components that require multiple selection, single selection, the type definition of value will be more different.

For such scenarios, you can use Merlion UI - useCheckControllableValue[11] to smooth out.

Component design

Service Request(⭐️⭐️⭐️⭐️⭐️)

This is a component design that is often encountered in business component design, for many scenarios, perhaps we just need to replace the URL of the request, so we have an API design like the following:

export type IAsyncProps {
  requestUrl?: string;
  extParams?: any;
}           

After the increase of access parties, the backend API result does not conform to the component parsing logic, or it is necessary to request multiple API combinations to obtain the data required by the component, so a simple request has the following parameters:

export type IAsyncProps {
  requestUrl?: string;
  extParams?: any;
  beforeUpload?: (res: any) => any
  format?: (res: any) => any
}           

This is just one of the requests, what if your business components need 2, 3? The API of the component will become more and more numerous, more and more complex, and the component will slowly become unusable and lifeless. 

The best API design practice for asynchronous interfaces should be to provide a promise method and define its input and exit parameter types in detail.

export type ProductList = {
  total: number;
  list: Array<{
    id: string;
    name: string;
    image: string;
    ...
  }>
}
export type AsyncGetProductList = (
  pageInfo: { current: number; pageSize: number },
  searchParams: { name: string; id: string; },
) => Promise<ProductList>;
export type IComponentProps = {
/**
   * The service to get product list
   */
  loadProduct?: AsyncGetProductList;
}           

After such a parameter definition, only 1 parameter is exposed to the public, and the parameter type is an async method. The developer needs to upload a function that conforms to the above input and exit parameter type definitions.

When used, the component does not care how the request occurs and what method is used in the request, the component only cares that the result returned conforms to the type definition.

This is completely white-box for developers working with components, with a clear view of what needs to be downcast, friendly error messages, and so on.

Hooks(⭐️⭐️⭐️⭐️⭐️)

Many times, maybe you don't need components!

For many business components, in many cases, we just encapsulate a superficial layer of business service features on the basis of the original components, such as:

  • Lazada Uploader:Upload + Lazada Upload Service
  • Address Selector: Select + Address Service
  • Brand Selector: Select + Brand Service
  • ...

For such shallow glue components, the component packaging is actually very fragile. Because the business will have various adjustments to the UI, for such components with extremely low rewriting costs, it is easy to lead to a proliferation of garbage parameters of the component.

In fact, a better way to encapsulate this kind of state encapsulation of service logic is to wrap it as React Hooks, such as uploading:

export function Page() {
const lzdUploadProps = useLzdUpload({ bu: 'seller' });
return <Upload {...lzdUploadProps} />
}           

Such encapsulation ensures both a high degree of logic reusability and UI flexibility.

Consumer(⭐️⭐️⭐️)

For cases where the component context needs to be used in the slot, we can consider using the consumer's design for component parameter design.

For example, the Expand component is designed so that part of the content is not displayed when it is put away. 

How to develop components that everyone loves?

For this type of component, obviously the content inside the container needs to get the key attribute isExpand to decide what to render, so we can consider designing it as a slot that accepts a callback function when designing the component:

export type IExpandProps = {
  children?: (ctx: { isExpand: boolean }) => React.ReactNode;
}           

On the consumption side, you can easily consume in the following ways:

export function Page() {
return (
<Expand>
      {({ isExpand }) => {
        return isExpand ? <Table /> : <AnotherTable />;
      }}
</Expand>
  );
}           

Document design

package.json(⭐️⭐️⭐️⭐️⭐️)

Make sure your repository is the correct repository address, because the configuration here is the only way to trace the source of many platforms, such as: npmjs.com\npm.alibaba-inc.com\mc.lazada.com

How to develop components that everyone loves?

Make sure that there are common entry definitions in package.json, such as main\module\types\exports, and here is a demonstration of package.json:

{
"name": "xxx-ui",
"version": "1.0.0",
"description": "Out-of-box UI solution for enterprise applications from B-side.",
"author": "[email protected]",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
    }
  },
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/cjs/index.d.ts",
"repository": {
"type": "git",
"url": "[email protected]:yee94/xxx.git"
  }
}           

README.md(⭐️⭐️⭐️⭐️)

If you're working on a library and want someone to use it, please provide at least a description of your library, a template has been generated for you in our scaffolding template, and the online DEMO address will be automatically included during the compilation process, but please add at least a description to it if you can. 

There are many ways to write here, if you really don't know how to write, you can find some well-known open source libraries to refer to, such as 'antd' \ 'react' \ 'vue' and so on.

There is another way, maybe you can ask for the help of 'ChatGPT', try it again and again.

Reference Links:

[1]https://www.npmjs.com/package/classnames

[2]https://www.npmjs.com/package/clsx

[3]https://jsdoc.app/

[4]https://vitdocjs.github.io/

[5]https://github.com/features/copilot

[6]https://segmentfault.com/a/1190000040308582

[7]https://ahooks.js.org/hooks/use-controllable-value

[8]https://ant.design/

[9]https://github.com/alibaba-fusion/next

[10]https://mc.lazada.com/package/@ali/merlion-ui#/src/hooks/use-selection/README.md

[11]https://mc.lazada.com/package/@ali/merlion-ui#/src/hooks/use-selection/README.md