实现思路
通过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)
总结
是不是很方便呢