diff --git a/src/index.tsx b/src/index.tsx index 9e4f5b7..ccc9683 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,7 +11,20 @@ * @module NiceModal * */ -import React, { useEffect, useCallback, useContext, useReducer, useMemo, ReactNode } from 'react'; +import React, { + useEffect, + useCallback, + useContext, + useReducer, + useMemo, + createContext, + FC, + Dispatch, + ReactNode, + ComponentProps, + ComponentType, + JSXElementConstructor +} from 'react'; export interface NiceModalState { id: string; @@ -89,21 +102,26 @@ export interface NiceModalHocProps { } const symModalId = Symbol('NiceModalId'); const initialState: NiceModalStore = {}; -export const NiceModalContext = React.createContext(initialState); -const NiceModalIdContext = React.createContext(null); +const DEFAULT_DISPATCH = () => { + throw new Error('No dispatch method detected, did you embed your app with NiceModal.Provider?'); +}; +export const NiceModalContext = createContext(initialState); +export const DispatchContext = createContext>(DEFAULT_DISPATCH); +const NiceModalIdContext = createContext(null); const MODAL_REGISTRY: { [id: string]: { - comp: React.FC; + comp: FC; props?: Record; }; } = {}; -const ALREADY_MOUNTED = {}; - -let uidSeed = 0; -let dispatch: React.Dispatch = () => { - throw new Error('No dispatch method detected, did you embed your app with NiceModal.Provider?'); -}; +const ALREADY_MOUNTED: Record = {}; const getUid = () => `_nice_modal_${uidSeed++}`; +let uidSeed = 0; +/** + * @deprecated We will deprecate this API because it encounters reference errors in nested provider scenarios. + * @see useModal() + */ +let deprecated_dispatch: Dispatch = DEFAULT_DISPATCH; // Modal reducer used in useReducer hook. export const reducer = ( @@ -160,7 +178,7 @@ export const reducer = ( }; // Get modal component by modal id -function getModal(modalId: string): React.FC | undefined { +function getModal(modalId: string): FC | undefined { return MODAL_REGISTRY[modalId]?.comp; } @@ -207,7 +225,7 @@ function removeModal(modalId: string): NiceModalAction { const modalCallbacks: NiceModalCallbacks = {}; const hideModalCallbacks: NiceModalCallbacks = {}; -const getModalId = (modal: string | React.FC): string => { +const getModalId = (modal: string | FC): string => { if (typeof modal === 'string') return modal as string; if (!modal[symModalId]) { modal[symModalId] = getUid(); @@ -215,29 +233,31 @@ const getModalId = (modal: string | React.FC): string => { return modal[symModalId]; }; -type NiceModalArgs = T extends keyof JSX.IntrinsicElements | React.JSXElementConstructor - ? React.ComponentProps +type NiceModalArgs = T extends keyof JSX.IntrinsicElements | JSXElementConstructor + ? ComponentProps : Record; -export function show>>>( - modal: React.FC, +export function show>>>( + modal: FC, args?: P, + dispatch?: Dispatch, ): Promise; -export function show(modal: string, args?: Record): Promise; -export function show(modal: string, args: P): Promise; +export function show(modal: string, args?: Record, dispatch?: Dispatch): Promise; +export function show(modal: string, args: P, dispatch?: Dispatch): Promise; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function show( - modal: React.FC | string, - args?: NiceModalArgs> | Record, + modal: FC | string, + args?: NiceModalArgs> | Record, + dispatch?: Dispatch ) { const modalId = getModalId(modal); if (typeof modal !== 'string' && !MODAL_REGISTRY[modalId]) { - register(modalId, modal as React.FC); + register(modalId, modal as FC); } - dispatch(showModal(modalId, args)); + (dispatch || deprecated_dispatch)(showModal(modalId, args)); if (!modalCallbacks[modalId]) { // `!` tell ts that theResolve will be written before it is used let theResolve!: (args?: unknown) => void; @@ -256,11 +276,11 @@ export function show( return modalCallbacks[modalId].promise; } -export function hide(modal: string | React.FC): Promise; +export function hide(modal: string | FC, dispatch?: Dispatch): Promise; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function hide(modal: string | React.FC) { +export function hide(modal: string | FC, dispatch?: Dispatch) { const modalId = getModalId(modal); - dispatch(hideModal(modalId)); + (dispatch || deprecated_dispatch)(hideModal(modalId)); // Should also delete the callback for modal.resolve #35 delete modalCallbacks[modalId]; if (!hideModalCallbacks[modalId]) { @@ -281,20 +301,21 @@ export function hide(modal: string | React.FC) { return hideModalCallbacks[modalId].promise; } -export const remove = (modal: string | React.FC): void => { +export const remove = (modal: string | FC, dispatch?: Dispatch): void => { const modalId = getModalId(modal); - dispatch(removeModal(modalId)); + + (dispatch || deprecated_dispatch)(removeModal(modalId)); delete modalCallbacks[modalId]; delete hideModalCallbacks[modalId]; }; const setFlags = (modalId: string, flags: Record): void => { - dispatch(setModalFlags(modalId, flags)); + deprecated_dispatch(setModalFlags(modalId, flags)); }; export function useModal(): NiceModalHandler; export function useModal(modal: string, args?: Record): NiceModalHandler; -export function useModal>>>( - modal: React.FC, +export function useModal>>>( + modal: FC, args?: P, ): Omit & { show: (args?: P) => Promise; @@ -303,6 +324,7 @@ export function useModal { if (isUseComponent && !MODAL_REGISTRY[mid]) { - register(mid, modal as React.FC, args); + register(mid, modal as FC, args); } }, [isUseComponent, mid, modal, args]); const modalInfo = modals[mid]; - const showCallback = useCallback((args?: Record) => show(mid, args), [mid]); - const hideCallback = useCallback(() => hide(mid), [mid]); - const removeCallback = useCallback(() => remove(mid), [mid]); + const showCallback = useCallback((args?: Record) => show(mid, args, dispatch), [mid]); + const hideCallback = useCallback(() => hide(mid, dispatch), [mid]); + const removeCallback = useCallback(() => remove(mid, dispatch), [mid]); const resolveCallback = useCallback( (args?: unknown) => { modalCallbacks[mid]?.resolve(args); @@ -378,8 +400,8 @@ export function useModal(modal?: any, args?: any): any { ); } export const create =

( - Comp: React.ComponentType

, -): React.FC

=> { + Comp: ComponentType

, +): FC

=> { return ({ defaultVisible, keepMounted, id, ...props }) => { const { args, show } = useModal(id); @@ -425,7 +447,7 @@ export const create =

( }; // All registered modals will be rendered in modal placeholder -export const register = >( +export const register = >( id: string, comp: T, props?: Partial>, @@ -447,7 +469,7 @@ export const unregister = (id: string): void => { // The placeholder component is used to auto render modals when call modal.show() // When modal.show() is called, it means there've been modal info -const NiceModalPlaceholder: React.FC = () => { +const NiceModalPlaceholder: FC = () => { const modals = useContext(NiceModalContext); const visibleModalIds = Object.keys(modals).filter((id) => !!modals[id]); visibleModalIds.forEach((id) => { @@ -475,35 +497,43 @@ const NiceModalPlaceholder: React.FC = () => { ); }; -const InnerContextProvider: React.FC = ({ children }) => { - const arr = useReducer(reducer, initialState); - const modals = arr[0]; - dispatch = arr[1]; +const InnerContextProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [modals, dispatch] = useReducer(reducer, initialState); + + deprecated_dispatch = dispatch; + return ( - {children} - + + {children} + + ); }; -export const Provider: React.FC> = ({ +export const Provider: FC<{ + children: ReactNode, + modals?: NiceModalStore, + dispatch?: Dispatch, + [key: string]: unknown, +}> = ({ children, dispatch: givenDispatch, modals: givenModals, -}: { - children: ReactNode; - dispatch?: React.Dispatch; - modals?: NiceModalStore; }) => { if (!givenDispatch || !givenModals) { return {children}; } - dispatch = givenDispatch; + + deprecated_dispatch = givenDispatch; + return ( - {children} - + + {children} + + ); }; @@ -514,12 +544,12 @@ export const Provider: React.FC> = ({ * @param component - The modal Component. * @returns */ -export const ModalDef: React.FC> = ({ +export const ModalDef: FC<{ + id: string, + component: FC, +}> = ({ id, component, -}: { - id: string; - component: React.FC; }) => { useEffect(() => { register(id, component); @@ -541,14 +571,14 @@ export const ModalDef: React.FC> = ({ * @param handler - The handler object to control the modal. * @returns */ -export const ModalHolder: React.FC> = ({ +export const ModalHolder: FC<{ + modal: string | FC; + handler: any; + [key: string]: unknown; +}> = ({ modal, handler = {}, ...restProps -}: { - modal: string | React.FC; - handler: any; - [key: string]: any; }) => { const mid = useMemo(() => getUid(), []); const ModalComp = typeof modal === 'string' ? MODAL_REGISTRY[modal]?.comp : modal; @@ -673,4 +703,4 @@ const NiceModal = { bootstrapDialog, }; -export default NiceModal; +export default NiceModal; \ No newline at end of file