Why do we need a UI component library

  • Improve team development efficiency
  • Improve system reusability and maintainability
  • Unify the way basic elements of a page interact
  • Unified page style, so that the output of products more professional
  • Meet some custom requirements

What makes a good UI component library

At the macro level, what does a good component library need to meet the goals we set out above?

  • A good abstraction of the business supports most business scenarios
  • Standard Api design, reduce user learning costs
  • Flexible design, strong expansibility, easy maintenance
  • Preview site, detailed documentation, easy access to apis and usage
  • No rendering and performance issues

How do we design and develop a good component library

Design principles

  • Single responsibility. The more single the responsibility, the more common the component
  • Independence, minimizing business coupling
  • API design and naming need to follow specifications to reduce learning costs
  • Flexible API for most scenarios
  • Ring free dependence

Hierarchy and relevance

Before we start developing a component, we should deconstruct the component, understand what parts are needed in the component, and understand the hierarchy and relationships among them. To make sure that we can implement their previous logical relationship through the API and assemble all the parts into a complete component. We can show these structures and relationships in the form of component diagrams, so that we have something to refer to when we start, and it is easier to make the application conform to our expectations.

To show components in a diagram, we can use these properties to represent a complete component or a part:

  • State
  • Props
  • Methods
  • Component/part relationships

Let’s break up the Table component

This gives our table component the ability to display, sort, merge cells, customize row elements, and customize column elements from top to bottom. We only need to implement each property according to the defined component diagram. It is also possible to represent hierarchy and relationships within components by defining an interface for each structure first.

Flattened State and Props

If State is defined as a complex object, it is likely that changing State will result in unnecessary rendering, and performance may be affected. For Props, if a prop structure is defined too complex, it is difficult for us to understand the internal details, resulting in unclear functions of components.

------ not good ------
interface AlertProps {     
	config: AlertConfig;         
    prefixCls: string;
}
interface AlertConfig {
	id: string;
    className: string;
    type: AlertType;
    visible: boolean;
    fixed: boolean;
    delay: number;
    content: any;
}

----- is good ------
interface AlertProps {
	id: string;
    className: string;
    type: AlertType;
    visible: boolean;
    fixed: boolean;
    delay: number;
    content: any;
    prefixCls: string;
}
Copy the code

However, for some scenarios, we need to combine some props together to reduce the number of props. For example, A component A needs to contain A button component, and external control of the button component is required. Should we split the buttonProps inside the PROPS of A or define A buttonProps property for external pass?

interface DropdownProps { menu: any[]; align: 'left' | 'right'; disabled: boolean; buttonProps: ButtonProps; } or interface DropdownProps { menu: any[]; align: 'left' | 'right'; disabled: boolean; . buttonProps... disabled? : boolean; checked? : boolean; label? : React.ReactNode; . buttonProps... }Copy the code

My answer is to define A buttonProps property, because component A doesn’t care about the properties inside the buttonProps, or doesn’t affect the properties inside the buttonProps, so the properties as A whole are enough to control the rendering and logic of the button.

Hide internal details and control components via props

Simply put, the state, events, and DOM of a component are hidden inside the component and cannot be accessed by the parent. Events that cannot be handled internally by the component, and states that depend on external controls, should be received from the parent as props. As far as possible, do not control component details in the form of REF or DOM manipulation.

Decoupled from the business, defining abstract components that are logically independent

It is common to develop a new component from a specific requirement, but if we only design the component for a specific business requirement, we find it difficult to apply that component to another similar business scenario. So we need to take a higher perspective to separate components from specific businesses. The following is a multi-business scenario for tags in different states.To analyze the hierarchy of components without business, we can use UML diagrams or interface definitions to reflect the hierarchical relationship. Here we choose the interface definition

interface BadgeProps {
	type: string;
    icon: IconProps;
    text: string;
}
interface IconProps {
	content: string;
	independent: boolean;
}
Copy the code

How should the type the type of definition, if it is defined according to the business, the type of optional values have draft | approved | declined | submitted. When this component is needed to represent other state tokens, we add the optional value of the Type field, along with the corresponding style change. This leads to a decrease in the reusability of our component and a lack of clarity in the API.

interface BadgeProps {
    type: 'light' | 'dark' | 'success' | 'warning' | 'danger';
    background: 'fill' | 'transparent';
    icon: React.ReactNode;
    children: React.ReactNode;
}
Copy the code

So we split the simple UI view out of the business, and all the state markers have the same structure, one icon, one text, and one background. Type controls the component’s font color and background color, and background controls how the component’s background is filled. We changed the ICONS and text from the qualified Icon component and text to any node node passed in from the outside to enhance the freedom and versatility of the component. In this way we can satisfy the representation of all state markers. For example,

<Badge type="success" icon={<Icon content="check-line" />} background="fill">Approved</Badge>
<Badge type="warning" icon={<Icon content="error-warning-fill" />} background="transparent">Pending</Badge>
Copy the code

Enhance the Api and extend component defaults and behavior

Enhancing the Api means setting the optional values of the Api to multiple possibilities, or combining two or more apis to satisfy multiple scenarios without increasing the complexity of the component Api. For example, when we were developing a drop-down component, we needed a searchable option



In this case, we need a handleSearchChange method inside the component to respond to changes in the value of the search box. By default, we should need such a method to fuzzy match the values in the drop-down box according to the input value of the search box.

options.filter(option= > option.label.includes(searchKey))
Copy the code

But what if we don’t need or need some other way of matching? What if the values we have selected are also displayed in the filter result list? What if our filter results are computed or called back end interfaces? How do we implement this requirement? In this case, we can add a filterOption property. The accepted value can be a property or method

interface SelectProps {
	filterOption: boolean | ((searchKey: string, option: OptionProps)) = > boolean
}
const optionFilter = (searchKey: string, option: OptionProps) = > {
	if (typeof filterOption === 'boolean') {
    	return filterOption || option.label.includes(searchKey);
    }
    return filterOption;
}
options.filter(option= > optionFilter(searchText, option))
Copy the code

This allows us to implement multiple filter options through an API. When we are doing the internal details of the component, we can think more about whether it needs to be set as a variable parameter when setting the default value and default behavior for the component. Are there potential business scenarios that would require us to change the default values and behavior? If so, we could have made the API Settings more flexible earlier. Don’t worry about over-design, there is no over-design, only inappropriate design.

Use extension components instead of adding a lot of adaptation code to the original component

Sometimes, in order to meet a particular business scenario, we need to add a lot of judgment and adaptation codes to the original component. At this time, we might as well consider adding an extension component, and make another layer of encapsulation based on the original component as the extension component of this component. For example, the Input component and NumberInput component, we can put the basic Input functions in the Input component, and the NumberInput constraints, formatting, and so on wrapped in NumberInput as an extension of the Input component.

Object-oriented component design

When designing complex components, we can adopt object-oriented thinking mode and divide complex components into objects and their relations. In addition, we should keep our object design to a minimum, making sure that each object only needs to do the simplest, core logic, and temporarily give up flexibility and freedom. Assemble the objects and then customize them.

Here’s an example of a date control:For a date control, there will be a lot of internal event and state maintenance, and the interaction will be complicated. Therefore, we need to split the component first. Which states need to rely on external props to pass in, and which states need to be managed by the component itself. Which events are exposed to the component for external processing and which are handled internally by the interaction logic itself. How to divide the granularity of components within a component? We still use UML diagrams to represent hierarchies.

Let’s just think about a single date

interface DayProps {
    date: Moment;  // What is the date to renderSelected: my Moment;// The selected date, in order to facilitate the selected style differentiation
    onClick: () = > void; // day The selected event is exposed to the upper layer for event processing
}
interface WeekProps {
    date: Moment;  // startOfWeek to render
    selected: Moment;  // The selected date is passed through to Day
    onDayClick: () = > void;  // Day's click event is passed through to DayRenderDays () {interface MonthProps {renderDays() {date: Moment;	// Render the month of startOfMonth
    selected: Moment; 
    onDayClick: (day: Moment) = > void; RenderWeeks () {interface HeaderProps {renderWeeks() {date: Moment;  // Displays the current month
    onPrevClick: () = > void;  // The button click event of the previous month is exposed to the upper layer processing
    onNextClick: () = > void;  // Next month button click eventRenderWeeksday () {interface CalendarProps {openToDate: Moment;  // The month used to initialize the display
    selected: Moment; // The selected date
    onChange: (day: Moment) = > void; // Process the event for which day is selected
}
interface CalendarStateMethod {
    date: Moment;  // The month currently displayed
    handleNavigationClick() // Handle events that switch months
}
Copy the code

At this point, we have a simple model of the date selection component, implemented according to the defined objects and interfaces. We also consider adding some functions, such as selecting a date range, so we need a start time and end time state, a mouse hover state to identify the selected range. The state and control logic we need to add to the corresponding object



We promoted the hoverDay state to a Calendar object to handle the event logic of clearing hoverDay when the mouse moved out. Pass startDate, endDate to props all the way to Day. Day object according to the startDate, endDate, hoverDay three status determine the current Day in the selected/are selected/not selected state. The selected date range is returned via onChange in Calendar handleChange

In the same way, we can add component functionality one by one, from core functionality, to function switches, to open the DOM for external definition. State can be designed in this way and decomposed into individual objects.

How to build a component preview site

There are many third-party plug-ins to choose from

  • Docz: React technology stack, MDX (Markdown + JSX) syntax, based on Gatsby.
  • Storybook: Supports Vue/React/Angular and addons to enhance document interaction.
  • React Styleguidist: React stack, which supports parsing JS/JSX code blocks in MD files.

Storybook is a platform that inherits many plugins, many of which are officially provided

Docs

🔗 NPM stroybook/addon-docs Addon-docs can present the interface defined by a component as an API in a document

MDX

🔗 NPM Stroybook-Adon-MDX-Embed MDX is a combination of Markdown and JSX format that can be used to write component stories

Action

🔗 NPM Addon-Actions Addon-Action displays the Event Handler events returned from the component and displays the parameters and calls in the Actions panel

Knobs

🔗 NPM addon-knobs Addon-Knobs allows users to operate the Props of components directly in the Knobs panel and dynamically change the UI

Storysource

🔗 NPM Addon-Storysource Addon-Storysource displays the source code for components in the Story panel

There are also some third-party plugins in the Storybook community that are very useful. You can go to the Storybook website and view the documentation using storybook.js.org/

conclusion

In addition to the UI component library, we have many types of components. Business components, logical components, higher-order components. These components have different roles and can be used to address code reuse issues in different scenarios. But there are also some design principles and norms that need to be followed.