Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the issue where some dialogs cannot be closed when using nested providers #157

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 92 additions & 62 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -89,21 +102,26 @@ export interface NiceModalHocProps {
}
const symModalId = Symbol('NiceModalId');
const initialState: NiceModalStore = {};
export const NiceModalContext = React.createContext<NiceModalStore>(initialState);
const NiceModalIdContext = React.createContext<string | null>(null);
const DEFAULT_DISPATCH = () => {
throw new Error('No dispatch method detected, did you embed your app with NiceModal.Provider?');
};
export const NiceModalContext = createContext<NiceModalStore>(initialState);
export const DispatchContext = createContext<Dispatch<NiceModalAction>>(DEFAULT_DISPATCH);
const NiceModalIdContext = createContext<string | null>(null);
const MODAL_REGISTRY: {
[id: string]: {
comp: React.FC<any>;
comp: FC<any>;
props?: Record<string, unknown>;
};
} = {};
const ALREADY_MOUNTED = {};

let uidSeed = 0;
let dispatch: React.Dispatch<NiceModalAction> = () => {
throw new Error('No dispatch method detected, did you embed your app with NiceModal.Provider?');
};
const ALREADY_MOUNTED: Record<string, boolean> = {};
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<NiceModalAction> = DEFAULT_DISPATCH;

// Modal reducer used in useReducer hook.
export const reducer = (
Expand Down Expand Up @@ -160,7 +178,7 @@ export const reducer = (
};

// Get modal component by modal id
function getModal(modalId: string): React.FC<any> | undefined {
function getModal(modalId: string): FC<any> | undefined {
return MODAL_REGISTRY[modalId]?.comp;
}

Expand Down Expand Up @@ -207,37 +225,39 @@ function removeModal(modalId: string): NiceModalAction {

const modalCallbacks: NiceModalCallbacks = {};
const hideModalCallbacks: NiceModalCallbacks = {};
const getModalId = (modal: string | React.FC<any>): string => {
const getModalId = (modal: string | FC<any>): string => {
if (typeof modal === 'string') return modal as string;
if (!modal[symModalId]) {
modal[symModalId] = getUid();
}
return modal[symModalId];
};

type NiceModalArgs<T> = T extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>
? React.ComponentProps<T>
type NiceModalArgs<T> = T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>
? ComponentProps<T>
: Record<string, unknown>;

export function show<T extends any, C extends any, P extends Partial<NiceModalArgs<React.FC<C>>>>(
modal: React.FC<C>,
export function show<T extends any, C extends any, P extends Partial<NiceModalArgs<FC<C>>>>(
modal: FC<C>,
args?: P,
dispatch?: Dispatch<NiceModalAction>,
): Promise<T>;

export function show<T extends any>(modal: string, args?: Record<string, unknown>): Promise<T>;
export function show<T extends any, P extends any>(modal: string, args: P): Promise<T>;
export function show<T extends any>(modal: string, args?: Record<string, unknown>, dispatch?: Dispatch<NiceModalAction>): Promise<T>;
export function show<T extends any, P extends any>(modal: string, args: P, dispatch?: Dispatch<NiceModalAction>): Promise<T>;

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function show(
modal: React.FC<any> | string,
args?: NiceModalArgs<React.FC<any>> | Record<string, unknown>,
modal: FC<any> | string,
args?: NiceModalArgs<FC<any>> | Record<string, unknown>,
dispatch?: Dispatch<NiceModalAction>
) {
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;
Expand All @@ -256,11 +276,11 @@ export function show(
return modalCallbacks[modalId].promise;
}

export function hide<T>(modal: string | React.FC<any>): Promise<T>;
export function hide<T>(modal: string | FC<any>, dispatch?: Dispatch<NiceModalAction>): Promise<T>;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function hide(modal: string | React.FC<any>) {
export function hide(modal: string | FC<any>, dispatch?: Dispatch<NiceModalAction>) {
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]) {
Expand All @@ -281,20 +301,21 @@ export function hide(modal: string | React.FC<any>) {
return hideModalCallbacks[modalId].promise;
}

export const remove = (modal: string | React.FC<any>): void => {
export const remove = (modal: string | FC<any>, dispatch?: Dispatch<NiceModalAction>): 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<string, unknown>): void => {
dispatch(setModalFlags(modalId, flags));
deprecated_dispatch(setModalFlags(modalId, flags));
};
export function useModal(): NiceModalHandler;
export function useModal(modal: string, args?: Record<string, unknown>): NiceModalHandler;
export function useModal<C extends any, P extends Partial<NiceModalArgs<React.FC<C>>>>(
modal: React.FC<C>,
export function useModal<C extends any, P extends Partial<NiceModalArgs<FC<C>>>>(
modal: FC<C>,
args?: P,
): Omit<NiceModalHandler, 'show'> & {
show: (args?: P) => Promise<unknown>;
Expand All @@ -303,6 +324,7 @@ export function useModal<C extends any, P extends Partial<NiceModalArgs<React.FC
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function useModal(modal?: any, args?: any): any {
const modals = useContext(NiceModalContext);
const dispatch = useContext(DispatchContext);
const contextModalId = useContext(NiceModalIdContext);
let modalId: string | null = null;
const isUseComponent = modal && typeof modal !== 'string';
Expand All @@ -319,15 +341,15 @@ export function useModal(modal?: any, args?: any): any {
// If use a component directly, register it.
useEffect(() => {
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<string, unknown>) => show(mid, args), [mid]);
const hideCallback = useCallback(() => hide(mid), [mid]);
const removeCallback = useCallback(() => remove(mid), [mid]);
const showCallback = useCallback((args?: Record<string, unknown>) => 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);
Expand Down Expand Up @@ -378,8 +400,8 @@ export function useModal(modal?: any, args?: any): any {
);
}
export const create = <P extends {}>(
Comp: React.ComponentType<P>,
): React.FC<P & NiceModalHocProps> => {
Comp: ComponentType<P>,
): FC<P & NiceModalHocProps> => {
return ({ defaultVisible, keepMounted, id, ...props }) => {
const { args, show } = useModal(id);

Expand Down Expand Up @@ -425,7 +447,7 @@ export const create = <P extends {}>(
};

// All registered modals will be rendered in modal placeholder
export const register = <T extends React.FC<any>>(
export const register = <T extends FC<any>>(
id: string,
comp: T,
props?: Partial<NiceModalArgs<T>>,
Expand All @@ -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) => {
Expand Down Expand Up @@ -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 (
<NiceModalContext.Provider value={modals}>
{children}
<NiceModalPlaceholder />
<DispatchContext.Provider value={dispatch}>
{children}
<NiceModalPlaceholder />
</DispatchContext.Provider>
</NiceModalContext.Provider>
);
};

export const Provider: React.FC<Record<string, unknown>> = ({
export const Provider: FC<{
children: ReactNode,
modals?: NiceModalStore,
dispatch?: Dispatch<NiceModalAction>,
[key: string]: unknown,
}> = ({
children,
dispatch: givenDispatch,
modals: givenModals,
}: {
children: ReactNode;
dispatch?: React.Dispatch<NiceModalAction>;
modals?: NiceModalStore;
}) => {
if (!givenDispatch || !givenModals) {
return <InnerContextProvider>{children}</InnerContextProvider>;
}
dispatch = givenDispatch;

deprecated_dispatch = givenDispatch;

return (
<NiceModalContext.Provider value={givenModals}>
{children}
<NiceModalPlaceholder />
<DispatchContext.Provider value={givenDispatch}>
{children}
<NiceModalPlaceholder />
</DispatchContext.Provider>
</NiceModalContext.Provider>
);
};
Expand All @@ -514,12 +544,12 @@ export const Provider: React.FC<Record<string, unknown>> = ({
* @param component - The modal Component.
* @returns
*/
export const ModalDef: React.FC<Record<string, unknown>> = ({
export const ModalDef: FC<{
id: string,
component: FC<any>,
}> = ({
id,
component,
}: {
id: string;
component: React.FC<any>;
}) => {
useEffect(() => {
register(id, component);
Expand All @@ -541,14 +571,14 @@ export const ModalDef: React.FC<Record<string, unknown>> = ({
* @param handler - The handler object to control the modal.
* @returns
*/
export const ModalHolder: React.FC<Record<string, unknown>> = ({
export const ModalHolder: FC<{
modal: string | FC<any>;
handler: any;
[key: string]: unknown;
}> = ({
modal,
handler = {},
...restProps
}: {
modal: string | React.FC<any>;
handler: any;
[key: string]: any;
}) => {
const mid = useMemo(() => getUid(), []);
const ModalComp = typeof modal === 'string' ? MODAL_REGISTRY[modal]?.comp : modal;
Expand Down Expand Up @@ -673,4 +703,4 @@ const NiceModal = {
bootstrapDialog,
};

export default NiceModal;
export default NiceModal;