使用自定义hooks,封装公共全局弹框

实现思路

通过useContext,provider实现全局的数据传递

  • app.js
import 'tslib'
import './index.css'
import App from './App'
import * as React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
// import reportWebVitals from './reportWebVitals'
import * as serviceWorker from './serviceWorker'
import { ActionProvider } from './utils/ActionProvider'
// 可以通过向仓库派发动作的方式实现路由跳转。
// 每次路径发生变化时可以把最新的路径放到仓库里面,以便随时在仓库中获取。
import { store } from './store'
import { actions } from '@/store/actions'

ReactDOM.render(
  // <React.StrictMode></React.StrictMode>  启动react的严格模式检查
  <Provider store={store}>
    <BrowserRouter>
      <ActionProvider actions={actions}>
          <App />
      </ActionProvider>
    </BrowserRouter>
  </Provider>,
  document.getElementById('root')
)
// reportWebVitals(console.log) // 性能监控 https://www.jianshu.com/p/9d75592edb9e
serviceWorker.unregister()

  • ActionProvider 封装
import React, { useState, useCallback, useMemo, Suspense, ReactElement, ReactNode } from 'react'
import { useLocation } from 'react-router-dom'
import { uuid } from './index'  // 生成uuid
import ActionContext from './ActionContext' // context
import useDidUpdate from '@/hooks/useDidUpdate' // 类似于componentDidupdate
import ActionLoading from './ActionLoading'
import ActionVisible from './ActionVisiable'

const DEFAULT_OPTIONS = {
  removeDelay: 300,
  initialVisible: true,
  Loading: ActionLoading,
  loadingProps: undefined
}

const NOOP = { update: () => {}, close: () => {} }

const isDevEnv = process.env.NODE_ENV === 'production'

if (isDevEnv) Object.freeze(NOOP)

interface Props {
  actions: Object[],
  removeDelay?: number,
  initialVisible?: boolean,
  Loading?: any,
  loadingProps?: any,
  children?: ReactNode
}

export const ActionProvider = ({
  actions,
  removeDelay,
  initialVisible,
  Loading,
  loadingProps,
  children }: Props): ReactElement => {
  const {
    pathname
  } = useLocation()

  const providerOptions = useMemo(() => {
    const result = { ...DEFAULT_OPTIONS }
    if (initialVisible !== undefined) result.initialVisible = initialVisible
    if (removeDelay !== undefined) result.removeDelay = removeDelay
    if (Loading !== undefined) result.Loading = Loading
    if (loadingProps !== undefined) result.loadingProps = loadingProps
    return result
  }, [Loading, initialVisible, loadingProps, removeDelay])

  const [visibleActions, setVisibleActions] = useState<any[]>([])

  const close = useCallback((type?:string, id?:string) => {
    if (type === undefined && id === undefined) {
      setVisibleActions([])
    } else {
      if (type !== undefined && id !== undefined) {
        setVisibleActions(visibleActions =>
          visibleActions.filter(
            visibleAction =>
              !(visibleAction.type === type && visibleAction.id === id)
          )
        )
      } else if (type !== undefined) {
        setVisibleActions(visibleActions =>
          visibleActions.filter(visibleAction => visibleAction.type !== type)
        )
      } else if (id !== undefined) {
        setVisibleActions(visibleActions =>
          visibleActions.filter(visibleAction => visibleAction.id !== id)
        )
      }
    }
  }, [])
  const open = useCallback(
    (type, props, options) => {
      if (isDevEnv) {
        if (typeof type !== 'string' || type === '') {
          console.error('`type` muse be an non-empty string')
        }
        if (props !== undefined) {
          if (typeof props !== 'object' || props === null) {
            console.error('`props` must be an object')
          }
          if (props.action !== undefined) {
            console.error(
              "You can't provide a prop named `action` for a global action component"
            )
          }
        }
        if (options !== undefined) {
          if (typeof options !== 'object' || options === null) {
            console.error('`options` must be an object')
          }
          if (options.id !== undefined) {
            if (typeof options.id !== 'string' || options.id === '') {
              console.error('`options.id` must be an non-empty string')
            }
          }
          if (options.removeDelay !== undefined) {
            if (
              typeof options.removeDelay !== 'number' ||
              options.removeDelay < 0 ||
              isNaN(options.removeDelay)
            ) {
              console.error(
                '`options.removeDelay` must be an integer greater than or equal to zero'
              )
            }
          }
        }
      }
     
      const newVisibleAction = {
        type,
        props,
        id: options && options.id !== undefined ? options.id : uuid(),
        ref: options && options.ref,
        initialVisible:
          options && options.initialVisible !== undefined
            ? options.initialVisible
            : providerOptions.initialVisible,
        removeDelay:
          options && options.removeDelay !== undefined
            ? options.removeDelay
            : providerOptions.removeDelay,
        Loading:
          options && options.Loading !== undefined
            ? options.Loading
            : providerOptions.Loading,
        loadingProps:
          options && options.loadingProps !== undefined
            ? options.loadingProps
            : providerOptions.loadingProps
      }

      if (
        visibleActions.find(
          visibleAction =>
            visibleAction.type === newVisibleAction.type &&
            visibleAction.id === newVisibleAction.id
        )
      ) {
        if (isDevEnv) {
          console.error(
            `Can't open action of the same \`type\`(${newVisibleAction.type}) and \`id\`(${newVisibleAction.id})`
          )
        }
      } else {
        setVisibleActions(visibleActions => [
          ...visibleActions,
          newVisibleAction
        ])
        return {
          update: (newProps: any) => {
            setVisibleActions(visibleActions => {
              visibleActions = [...visibleActions]
              const action = visibleActions.find(item => {
                return (
                  item.type === newVisibleAction.type &&
                  item.id === newVisibleAction.id
                )
              })
              if (action) {
                if (typeof newProps === 'function') {
                  action.props = newProps({ ...action.props })
                } else if (typeof newProps === 'object' && newProps != null) {
                  action.props = { ...action.props, ...newProps }
                } else {
                  if (isDevEnv) {
                    console.error('`newProps` must be an object or a function')
                  }
                }
              }
              return visibleActions
            })
          },
          close: () => {
            close(newVisibleAction.type, newVisibleAction.id)
          }
        }
      }
    
      return NOOP
    },
    [
      close,
      providerOptions.Loading,
      providerOptions.initialVisible,
      providerOptions.loadingProps,
      providerOptions.removeDelay,
      visibleActions
    ]
  )


  const providerValue = useMemo(
    () => ({
      actions,
      visibleActions,
      open,
      close
    }),
    [actions, close, open, visibleActions]
  )

  const renderVisibleActions = useMemo(() => {
    return providerValue.visibleActions
      .map(visibleAction => {
        const action = actions.find(
          (_action: any) => {
            return _action.type === visibleAction.type
          }
        )

        if (action) {
          return (
            <Suspense
              key={visibleAction.id}
              fallback={
                <ActionVisible
                  id={visibleAction.id}
                  type={visibleAction.type}
                  initialVisible={false}
                  removeDelay={visibleAction.removeDelay}
                  Component={visibleAction.Loading}
                  componentProps={visibleAction.loadingProps}
                  closeAction={close}
                />
              }
            >
              <ActionVisible
                id={visibleAction.id}
                type={visibleAction.type}
                initialVisible={visibleAction.initialVisible}
                removeDelay={visibleAction.removeDelay}
                Component={action.component}
                componentProps={visibleAction.props}
                closeAction={close}
                forwardRef={visibleAction.ref}
              />
            </Suspense>
          )
        } else {
          if (isDevEnv) {
            console.error(`Can't find the \`${visibleAction.type}\` action.`)
          }
        }
        return null
      })
      .filter(Boolean)
  }, [actions, close, providerValue.visibleActions])

  useDidUpdate(() => {
    close()
  }, [pathname] as any)

  return (
    <ActionContext.Provider value={providerValue as any}>
      {children}
      {renderVisibleActions}
    </ActionContext.Provider>
  )
}

  • action库
import { lazy } from 'react'
export const actions = [
  {
    type: 'TestModal',
    component: lazy(() => import('@/components/Modals/index'))
  }
]

  • ActionVisiable
import React, { memo, useState, useCallback, useMemo, ReactElement } from 'react'
import useTimeout from '@/hooks/useTimeout'

interface Props {
  id: string,
  type: string,
  Component: any,
  initialVisible: boolean,
  componentProps: any,
  closeAction: any,
  removeDelay: number,
  forwardRef?: any
}

function ActionVisiable ({
  id,
  type,
  Component,
  initialVisible,
  componentProps,
  closeAction,
  removeDelay,
  forwardRef
}: Props): ReactElement {
  const [visible, setVisible] = useState(!!initialVisible)
  const closeActionDelay = useCallback(() => {
    closeAction(type, id)
  }, [closeAction, id, type])
  const [setCloseActionTimeout] = useTimeout(closeActionDelay, removeDelay)
  const open = useCallback(() => {
    setVisible(true)
  }, [])
  const close = useCallback(() => {
    setVisible(false)
    setCloseActionTimeout()
  }, [setCloseActionTimeout])
  const injectedActionProps = useMemo(() => {
    return { visible, open, close, id, type }
  }, [close, id, open, type, visible])

  const content = (
    <Component
      {...componentProps}
      ref={forwardRef}
      action={injectedActionProps}
    />
  )
  return content
}

export default memo(ActionVisiable)

  • ActionLoading

import React, { memo, useMemo, ReactNode, FC, ReactElement } from 'react'
import { Drawer, Modal, Spin } from 'antd'
import useTimeout from '@/hooks/useTimeout'
import useDidMount from '@/hooks/useDidMount'

enum myType {
  'modal',
  'drawer'
}

interface Props {
  action: {
    visible: boolean,
    open: () => {},
    close: () => {},
  },
  type: myType,
  delay: number,
  children: ReactNode,
};

const myChildren: FC<any> = () => {
  const content = (
  <div style={{ textAlign: 'center', padding: '24px 0' }}>
    <Spin />
  </div>)
  return content
}

function ActionLoading ({ type = myType.modal, delay = 300, children = myChildren, action, ...rest }: Props): ReactElement {
  const { visible, open, close } = action
 
  const [startOpenTimer] = useTimeout(open, delay)
  useDidMount(startOpenTimer)

  const overlayProps = useMemo(() => {
    switch (type) {
      case myType.drawer:
        return {
          visible,
          onClose: close,
          destroyOnClose: true,
          ...rest
        }
      case myType.modal:
      default:
        return {
          visible,
          onCancel: close,
          footer: null,
          destroyOnClose: true,
          maskClosable: false,
          ...rest
        }
    }
  }, [close, rest, type, visible])

  return type === myType.drawer ? <Drawer {...overlayProps}>{children}</Drawer> : <Modal {...overlayProps}>{children}</Modal>
}

export default memo(ActionLoading)

  • useDidUpdate
import { useEffect, useRef } from 'react'
export default function useDidUpdate (fn: Function, deps = []) {
  const mountedRef = useRef(false)
  useEffect(
    () => {
      if (mountedRef.current) {
        fn()
      }
      if (!mountedRef.current) {
        mountedRef.current = true
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    deps
  )
}
  • useAction
import { useContext, useCallback } from 'react'
import ActionContext from '@/utils/ActionContext'
export default function useAction (defaultType: any, defaultOptions?: any) {
  const { open: openAction } = useContext(ActionContext)
  const open = useCallback(
    (type?: any, props?: any, options?: any) => {
      if (defaultType) {
        options = props
        props = type
        type = defaultType
      }
      if (defaultOptions) {
        options = { ...defaultOptions, ...options }
      }
      return openAction(type, props, options)
    },
    [defaultOptions, defaultType, openAction]
  )
  return open
}

使用方法

import React, { ReactElement } from 'react'

export default function MyModal (): ReactElement {

  const openModal = useAction('TestModal')
  
 const handleClick = React.useCallback(() => {
    openModal()
  }, [openModal])
  
  return (
    <div>
      <button onClick={handleClick}>打开modal</button>
    </div>
  )
}

  • TestModal
import React, { forwardRef, useCallback, ReactNode } from 'react'
import { Modal } from 'antd'

interface Props {
  action: any,
  onOk: any,
  children?: ReactNode,
  title: string
}

const ActionModal = ({ action, onOk, title, children, ...rest }: Props, ref?: any) => {
  const { visible, close } = action

  const handleOk = useCallback((e) => {
    if (onOk) {
      onOk(e)
    }
    close()
  }, [close, onOk])
  return (
    <Modal
      {...rest}
      ref={ref}
      title={title}
      visible={visible}
      onCancel={close}
      onOk={handleOk}
    >
      {children}
    </Modal>
  )
}

export default forwardRef(ActionModal)

总结

是不是很方便呢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MaxLoongLvs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值