React hooks基础组件的封装

173 阅读1分钟

前言

在做前端项目时,需要对基础组件进行一层封装,以方便自定义修改组件库如antd的组件属性和方法。

button组件

组件代码:

import { Button } from 'antd';
import { FC, memo, useRef } from 'react';
import styles from './index.less';

type ButtonType = 'black' | 'red' | 'gray';
const buttonTypeClass: any = {
    black: styles.blackButton, // 黑色主题
    red: styles.redButton, // 红色主题
    gray: styles.grayButton // 灰色主题
}
interface PropsType {
    onClick?: Function
    disabled?: boolean // 是否可点击
    repeatClick?: boolean // 是否允许重复提交(快速多次点击)
    repeatLimit?: number // 重复点击时长限制
    style?: object
    className?: string | object
    $ref?: any
    type?: ButtonType // 几种类型
    icon?: any
    htmlType?: any
    children?: any
}
const defaultOptions: PropsType = {
    disabled: false,
    repeatClick: false,
    repeatLimit: 1000
}

const _Button: FC<PropsType> = (props: PropsType) => {
    const { htmlType, icon, disabled = false, style = {}, repeatClick = false, className, onClick, repeatLimit = 1000, type = 'black' } = props    const timestampRef = useRef<number>(Date.now())
    const trigger = (e: any) => {
        e.stopPropagation()
        // 两次点击间隔小于repeatLimit视为重复点击
        if (disabled || (Date.now() - timestampRef.current < repeatLimit && !repeatClick)) return
        timestampRef.current = Date.now()
        onClick && onClick(e)
    }
    return (
        <Button
            className={`${styles.button} ${type ? buttonTypeClass[type] : ''} ${className || ''}`}
            style={{...style}}
            icon={icon}
            htmlType={htmlType}
            disabled={disabled}
            onClick={trigger}
        >
            {props.children ? props.children : ''}
        </Button>
    )
}

export const CustomButton = memo(_Button)
export default memo(_Button)

组件样式:

.button {
    text-align: center;
    padding: 13px 20px;
    border-radius: 4px;
    height: auto;
    line-height: 1;
    font-size: 14px;
    font-weight: 400;
    border: none;
    box-shadow: none;
    cursor: pointer;
}

.blackButton {
    color: white;
    background-color: @col0A0F16;
    &:hover {
        color: @white !important;
        background-color: @col3B3F45 !important;
   }
    &:disabled {
        color: @white !important;
        background-color: @colCECFD0 !important;
    }
}

.redButton {
    color: @white;
    background-color: @colD2001C;
    &:hover {
        color: @white !important;
        background-color: @colDB3349 !important;
    }
    &:disabled {
        color: @white !important;
        background-color: @colF6CBD2 !important;
    }
}

.grayButton {
    color: @col0A0F16;
    background-color: @colF2F3F8;
    &:hover {
        color: @col0A0F16 !important;
        background-color: @colE6E7F1 !important;
    }
    &:disabled {
        color: rgba(10, 15, 22, 0.40) !important;
        background-color: @colF7F8FB !important;
    }
}

全局toast组件

和button组件不一样的是,toast组件要求在js或ts代码中可以直接调用,思路:动态创建容器包裹toast组件并渲染。

import React, { useState, memo, useEffect } from 'react'
import { CSSTransition } from 'react-transition-group'
import styles from './index.module.less'
import { modernRender } from '../instance'

type positionType = 'top' | 'middle' | 'bottom'

interface PropsType {
    text?: string
    duration?: number
    position?: positionType
    customClass?: unknown
    iconClass?: string
    visible?: boolean
    $ref?: unknown
}

const defaultOptions: PropsType = {
    text: '',
    duration: 3000,
    position: 'middle',
    iconClass: '', // 通过class制定icon
    visible: false // 默认隐藏
}

const _Toast: React.FC<PropsType> = (props: PropsType) => {
    const { duration = 3000, position, customClass, iconClass, text } = props
    return (
        <CSSTransition in={visible} timeout={10} classNames="slide-up">
            <div
                className={`${styles.toastContainer} flex-center ${styles[position || '']} ${customClass || ''}`}
                style={{
                    display: `${visible ? 'block' : 'none'}`,
                    padding: `${iconClass ? '0' : '10px'}`,
                    animation: `${styles.opcity} ${duration / 1000}s linear`,
                    WebkitAnimation: `${styles.opcity} ${duration / 1000}s linear`
                }}
            >
                {iconClass && <i className={`${styles.iconClass}`}></i>}
                <div className={`${styles.toastText}`}>{text}</div>
            </div>
        </CSSTransition>
    )
}

_Toast.defaultProps = defaultOptions

export const Toast = memo(_Toast)

/** * 全局公共组件(单例) */
const getInstance = () => {
    let instance
    return () => {
        if (instance) {
            return instance
        }
        const div = document.createElement('div')
        document.body.appendChild(div)
        return (instance = (props: string | object) => {
            modernRender(
                React.createElement(Toast, { ...(typeof props === 'object' ? props : { text: props }), visible: true, key: Date.now() }),
                div
            )
        })
    }
}

export const AToast = getInstance()()

export default AToast

instance.tsx:

import React from 'react'
import { createRoot, Root } from 'react-dom/client'

// 容器
type Container = Element | DocumentFragment

type ExtendedRoot = Root & { unmountPromise?: Promise<boolean> }

// 容器对应 root 的 map, 因为 Container 类型问题所以使用 weakMap
const containerMap = new WeakMap<Container, ExtendedRoot>()

export async function modernRender(node: React.ReactElement, container: Container) {
    let root = containerMap.get(container)

    root = containerMap.has(container) ? (containerMap.get(container) as ExtendedRoot) : createRoot(container)

    containerMap.set(container, root)

    root.render(node)
}

export function modernUnmount(container: Container) {
    // Delay to unmount to avoid React 18 sync warning
    if (containerMap.has(container)) {
        const root = containerMap.get(container) as ExtendedRoot
        root?.unmount()
        containerMap.delete(container)
    }
}

组件样式:

.toastContainer {
    position: fixed;
    max-width: 90%;
    min-width: 60%;
    border-radius: 5px;
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
    box-sizing: border-box;
    text-align: center;
    z-index: -1;
    opacity: 0;
    &.top {
        top: 50px;
        left: 50%;
        transform: translate(-50%, 0);
    }
    &.middle {
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }
    &.bottom {
        bottom: 50px;
        left: 50%;
        transform: translate(-50%, 0);
    }
    .toastText {
        font-size: 28px;
        line-height: 40px;
        text-align: center;
        min-width: 500px;
    }
}

@keyframes opcity {
    0% {
        opacity: 0;
        z-index: 100;
    }
    2% {
        opacity: 1;
    }
    98% {
        opacity: 1;
    }
    100% {
        opacity: 0;
        z-index: -1;
    }
}