Article starts in my blog alfxjx. Gitee. IO/tech/design…

primers

Recently, the company is building a unified platform for the department, and I am responsible for the development of the unified login front end. Because there are many systems to connect with, I have developed the SDK for unified login, which is actually a component library. As a result, there are many different component libraries (UI/Vant/ANTd/Chakra-UI…) that have been used in other systems. Today, write an article about what kind of component design is reasonable, and how to design a useful component.

Research available – the UI

Chakra-ui is not widely used in China. I happened to find it in the selection of the technology before, but after reading her documents carefully, I found it to be a very distinctive component library. Here are the details:

Style customization

The chakra-UI was found because of the need to use a component library along with tailwind CSS. One of the features of chakra-UI is that it uses almost the same style API as tailwind CSS, for example:

import { Box } from "@chakra-ui/react"


// m={2} refers to the value of `theme.space[2]`
<Box m={2}>Tomato</Box>

// You can also use custom values
<Box maxW="960px" mx="auto" />

// sets margin `8px` on all viewports and `16px` from the first breakpoint and up
<Box m={[2, 3]} / >
Copy the code

As long as you remember the TAILwind CSS API, you will be able to learn chakra-UI quickly. So how does this work? At first I thought the component library used tailwind CSS, but looking at the source code, Chakra has styled- Components for the second time, and the tailwindLike API is simulated as a result. /packages/styled-system/config/

export const background: Config = {
  background: t.colors("background"),...bg: t.colors("background"),... }Copy the code

In this way, it can be said that the dirty work is wrapped up, and the results are easy to use.

Combination of components

In order to reduce the amount of code and unify the management, the component library carries on the component composition (compose). Start with a basic component and create some new ones by default. The Square Circle component, for example, is based on the Box component extend.

export const Square = forwardRef<SquareProps, "div"> ((props, ref) = > {
  const { size, centerContent = true. rest } = propsconst styles: SystemStyleObject = centerContent
    ? { display: "flex".alignItems: "center".justifyContent: "center"}, {}return (
    <Box
      ref={ref}
      boxSize={size}
      __css={{
        . styles.flexShrink: 0.flexGrow: 0,}} {. rest} / >)})if (__DEV__) {
  Square.displayName = "Square"
}

export const Circle = forwardRef<SquareProps, "div"> ((props, ref) = > {
  const{ size, ... rest } = propsreturn <Square size={size} ref={ref} borderRadius="9999px" {. rest} / >
})

if (__DEV__) {
  Circle.displayName = "Circle"
}
Copy the code

Writing this way reduces duplicate code and maintains better maintainability. We can also use this pattern in the development process to develop the most abstract component, derived from the most abstract parent class.

Theminig

Another feature of the Chakra UI is a highly customizable theme system, similar to the tailwind CSS setup, which means you can apply a theme file to both libraries at the same time. See the Chakra documentation for details. First, chakra UI maintains a default theme for merging when there is no custom theme or part of a custom theme. The merging process (toCSSVar) uses the createThemeVars method to convert a self-configured theme into a CSS VAR variable, and then merges the default theme with the generated theme. Finally, in:

export const ThemeProvider = (props: ThemeProviderProps) = > {
  const { cssVarsRoot = ":host, :root", theme, children } = props
  const computedTheme = React.useMemo(() = > toCSSVar(theme), [theme])
  return (
    <EmotionThemeProvider theme={computedTheme}>
      <Global styles={(theme: any) = > ({ [cssVarsRoot]: theme.__cssVars })} />
      {children}
    </EmotionThemeProvider>)}Copy the code

This is a ThemeProvider borrowed from emotion. In fact, the theme setting is very simple. This makes it easy to set up a custom Theme. In addition, if you want to do a third development on a Theme for secondary development, you can use the API Theme Extensions provided by Chakra-UI. Provides a wrap function similar to HOC, with withDefaultColorScheme as an example:

export function withDefaultColorScheme({colorScheme,components}) :ThemeExtension {
  return (theme) = > {
    let names = Object.keys(theme.components || {})
		/ /...
    return mergeThemeOverride(theme, {
      components: Object.fromEntries(
        names.map((componentName) = > {
          const withColorScheme = {
            defaultProps: {
              colorScheme,
            },
          }
          return [componentName, withColorScheme]
        }),
      ),
    })
  }
}
Copy the code

Assigns the configuration color Scheme to the corresponding component of the configuration. The internal implementation is pretty much the same, calling the mergeThemeOverride method

export function mergeThemeOverride<BaseTheme extends ChakraTheme = ChakraTheme> (. overrides: ThemeOverride
       
        []
       ) :ThemeOverride<BaseTheme> {
  returnmergeWith({}, ... overrides, mergeThemeCustomizer) }Copy the code

Internally, the lodash.mergewith method is used for the fusion. Chakra-ui writes a mergeThemeCustomizer as the third parameter to lodash.mergwith. The custom mergeThemeCustomizer method here uses a recursive approach to merge.


function mergeThemeCustomizer(
  source: unknown,
  override: unknown,
  key: string.object: any.) {
  if (
    (isFunction(source) || isFunction(override)) &&
    Object.prototype.hasOwnProperty.call(object, key)
  ) {
    return (. args: unknown[]) = > {
      constsourceValue = isFunction(source) ? source(... args) : sourceconstoverrideValue = isFunction(override) ? override(... args) : overridereturn mergeWith({}, sourceValue, overrideValue, mergeThemeCustomizer)
    }
  }

  // fallback to default behaviour
  return undefined
}
Copy the code

External components and secondary encapsulation

As can be seen from the above, the component library is not entirely from scratch. Many third party libraries are also used, such as the style library emotion, Styled – Components, and the utility method library LoDash. There is nothing special to be said here. Chakra-ui also recommends using component libraries with many third-party libraries, such as the formik form validation library, and packaging throttle-debounce, Async-Validator, and other third-party libraries directly in Element-UI.

Provide the escape

When using other component libraries, there are many situations where the details and design requirements of certain components are inconsistent. For Element-UI and Ant Design, due to the use of preprocessors such as SASS/Less, styles can be overridden. In the Chakra UI, an SX Props is provided to pass styles directly to the component.

<Box sx={{ "--my-color": "#53c8c4"}} ><Heading color="var(--my-color)" size="lg">
    This uses CSS Custom Properties!
  </Heading>
</Box>
Copy the code

This approach is powerful and supports nested styles, media Query, and more. Sx is a wrapper from here @ emotion/styled method, on the packages/system/SRC/system. Ts, styled method calls the toCSSObject inside, take to enter the style here, The Styled method is called by all UI components, and the SX Props is used globally.

export function styled<T extends As.P = {}>( component: T, options? : StyledOptions, ) {const{ baseStyle, ... styledOptions } = options ?? {}// ...
  const styleObject = toCSSObject({ baseStyle })
  return _styled(
    component as React.ComponentType<any>,
    styledOptions,
  )(styleObject) as ChakraComponent<T, P>
}

export const toCSSObject: GetStyleObject = ({ baseStyle }) = > (props) = > {
  const { theme, css: cssProp, __css, sx, ... rest } = propsconst styleProps = objectFilter(rest, (_, prop) = > isStyleProp(prop))
  const finalBaseStyle = runIfFn(baseStyle, props)
  const finalStyles = Object.assign({}, __css, finalBaseStyle, styleProps, sx)
  const computedCSS = css(finalStyles)(props.theme)
  return cssProp ? [computedCSS, cssProp] : computedCSS
}
Copy the code

How to design a usable component

With many designs in mind, how do you design a good component? Here’s an example of a progressBar.

MVP version and its problems

import React, { useState, useEffect } from "react";
import styled from "styled-components";

const ProgressBarWrapper = styled.div<{ progress: number} >`
	width: 100%;
	height: 4px;
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	z-index: 9999;
	.bar-used {
		background: #34c;
		width: ${({ progress }) => progress + "%"};
		height: 100%;
		border-radius: 0 2px 2px 0;
	}
`;

const ProgressBar = () = > {
	const [progress, setProgress] = useState(0);
	useEffect(() = > {
		window.addEventListener("scroll".() = > {
			setProgress(
				(document.documentElement.scrollTop /
					(document.body.scrollHeight - window.innerHeight)) *
					100
			);
		});
		return () = > {
			window.removeEventListener("scroll".() = > {});
		};
	});
	return (
		<ProgressBarWrapper progress={progress}>
			<div className='bar-used'></div>
		</ProgressBarWrapper>
	);
};

export { ProgressBar };

Copy the code

Here is a component of a progress bar at the top of the page, similar to the es6 standard introduction here. The above functionality can be implemented quickly, but it is more suitable for a single application scenario, where the progress bar is fixed at the top and only grows from left to right. However, the actual progress bar can be used in many places, so we need to compare the possible scenarios and variables in the code to determine which parameters need to be made, and set the corresponding default values. The requirements are as follows:

  1. Color adjustable, position adjustable, direction adjustable, these three are more global adjustable types
  2. Specific style modifications, height modifications, and rounded corners are other props that might not be needed if the ProgressBar function had been kept intact

Another problem with progeressBar is that it mixes presentation and logic. The component has a useEffect on the scrolling progress of the page, but what if you don’t need this logic? Let’s split and refactor this component based on the above changes and issues.

refactoring

The first is to separate logic from presentation. Create a new hook to calculate the percentage.

import { useState, useEffect } from "react";

export function useProgress() {
	const [progress, setProgress] = useState(0);
	useEffect(() = > {
		window.addEventListener("scroll".() = > {
			setProgress(
				(document.documentElement.scrollTop /
					(document.body.scrollHeight - window.innerHeight)) *
					100
			);
		});
		return () = > {
			window.removeEventListener("scroll".() = > {});
		};
	});
	return progress;
}

Copy the code

Then add props for the required parameters and set the default values. In this case, just use the height as an example. Set an optional height parameter that will be used when passed and default otherwise. Also note that colors and so on can use a theme system.


const ProgressBarWrapper = styled.div<{ progress: number; height? :string} >`
	width: 100%;
	height: ${({ height }) => (height ? height : "4px")};
	.bar-used {
		background: ${({ theme }) => theme.themeColor};
		width: ${({ progress }) => progress + "%"};
		height: 100%;
		border-radius: ${({ height }) =>
			height ? `0 calc( ${height}/ 2) calc(${height}0 ` / 2) : "0 2px 2px 0"};
	}
`;
Copy the code

In addition, fixed layout will be abstracted out, convenient for combination behind

const FixedTopWrapper = styled.div` position: fixed; top: 0; left: 0; right: 0; z-index: 9999; `;
// It looks like this
const ProgressBarWrapperFixed = styled(FixedTopWrapper)<{
	progress: number; height? :string; } >`... `;
Copy the code

The component looks like this, split between the ProgressBar, which is nice by default, and the SimpleProgressBar, which has more customizations


interface ProgressProps {
	progress: number; height? :string;
}
const ProgressBar = ({
	height,
}: Omit<ProgressProps, "progress">) = > {
	const progress = useProgress();
	return (
		<ProgressBarWrapperFixed progress={progress} height={height}>
			<div className='bar-used'></div>
		</ProgressBarWrapperFixed>
	);
};

const SimpleProgressBar = ({ progress, height, }: ProgressProps) = > {
	return (
		<ProgressBarWrapper progress={progress} height={height}>
			<div className='bar-used'></div>
		</ProgressBarWrapper>
	);
};
Copy the code

The other thing is to add the appropriate escape so that you can modify it if it doesn’t fit your needs. Here we add a style argument directly to the component

// usage
<ProgressBar style={{ background: "# 000" }}></ProgressBar>
Add the rest parameter to accept the additional style and modify the type
const ProgressBar = ({ height, ... rest }: Omit<ProgressProps,"progress"> & React.HTMLAttributes<HTMLDivElement>) = > {
	const progress = useProgress();
	return (
		<ProgressBarWrapperFixed {. rest} progress={progress} height={height}>
			<div className='bar-used'></div>
		</ProgressBarWrapperFixed>
	);
};
Copy the code

This leaves a nice ProgressBar component and provides SimpleProgressBar for other custom purposes. Online demo: Codepen. IO/ALFXJx/PEN /…

conclusion

After researching the source code of chakra UI component library and giving you an example, I believe you know how to design a useful component library. I hope you can develop a component library for your company to better meet your KPI/OKR /etc…

Reference

  1. Github.com/chakra-ui/c…
  2. chakra-ui.com/
  3. emotion.sh/
  4. www.lodashjs.com/
  5. tailwindcss.com/
  6. element.eleme.cn/#/zh-CN
  7. ant.design/index-cn
  8. Stackoverflow.com/questions/5…