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:
- Color adjustable, position adjustable, direction adjustable, these three are more global adjustable types
- 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
- Github.com/chakra-ui/c…
- chakra-ui.com/
- emotion.sh/
- www.lodashjs.com/
- tailwindcss.com/
- element.eleme.cn/#/zh-CN
- ant.design/index-cn
- Stackoverflow.com/questions/5…