Created
April 2, 2024 16:55
-
-
Save kylemh/d44f9033cff40bb32c3465730b6bfe07 to your computer and use it in GitHub Desktop.
This a pretty helpful thing to setup and expand upon for applications with loads of modals... It's helpful to hoist dialogs, snackbars, and toasts to a coalesced layer for upkeep and style uniformity (like animation timing).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createContext, useContext, useReducer, useMemo } from 'react'; | |
import type { ComponentProps, FunctionComponent, PropsWithChildren, Reducer, Dispatch } from 'react'; | |
import { AnimatePresence } from 'framer-motion'; | |
type ModalProps<T = {}> = T & PropsWithChildren<{ closeModal: () => void }>; | |
type ModalComponentType = FunctionComponent<ModalProps>; | |
interface ModalState { | |
Modal: FunctionComponent<ModalProps>; | |
props?: { | |
[key: string]: any; | |
}; | |
} | |
type ModalAction = | |
| { type: 'show'; Modal: ModalComponentType; props: ModalState['props'] } | |
| { type: 'hide'; Modal: ModalComponentType }; | |
const reducer: Reducer<ModalState[], ModalAction> = (state, action) => { | |
const otherModals = state.filter(({ Modal }) => Modal.name !== action.Modal.name); | |
switch (action.type) { | |
case 'show': | |
return [...otherModals, { Modal: action.Modal, props: action.props }]; | |
case 'hide': { | |
return otherModals; | |
} | |
default: | |
return state; | |
} | |
}; | |
const ModalProviderContext = createContext<Dispatch<ModalAction>>(() => {}); | |
export function ModalProvider({ children }: PropsWithChildren<{}>) { | |
const [state, dispatch] = useReducer(reducer, []); | |
return ( | |
<ModalProviderContext.Provider value={dispatch}> | |
{children} | |
<AnimatePresence> | |
{state.map(({ Modal, props }) => { | |
return <ModalComponentType closeModal={() => dispatch({ type: 'hide', Modal })} key={Modal.name} {...props} />; | |
})} | |
</AnimatePresence> | |
</ModalProviderContext.Provider> | |
); | |
} | |
/** | |
* @description This hook allows you to show/hide and modal component you want by just passing the component. | |
* It will return you a show and hide function respectively. | |
* | |
* @param Modal The modal component you want to show. You can wait for intellisense. | |
*/ | |
export function useModal< | |
TModal extends FunctionComponent<any>, | |
TModalProps extends Omit<ComponentProps<TModal>, keyof ModalProps>, | |
TOpenModalCallback extends keyof TModalProps extends never ? () => void : (props: TModalProps) => void, | |
>(Modal: TModal): [TOpenModalCallback, () => void] { | |
// in apps where code-splitting is important, the type signature for Modal can be adjusted to only accept loaders | |
// usage then would look like `const [openModal, closeModal] = useModal(() => import('./SomeCustomModalContent'));` | |
const dispatch = useContext(ModalProviderContext); | |
const values = useMemo<[TOpenModalCallback, () => void]>(() => { | |
const showFn: TOpenModalCallback = (props) => dispatch({ type: 'show', Modal, props }); | |
const hideFn = () => dispatch({ type: 'hide', Modal }); | |
return [showFn, hideFn]; | |
}, [dispatch, Modal]); | |
return values; | |
} | |
/** Example usage */ | |
const SomeComponent = () => { | |
const [openModalA] = useModal(ModalA) | |
const [openModalB] = useModal(ModalB) | |
// and with dynamic imports with some small adjustments: | |
const [openModalA] = useModal(() => import('./ModalA')) | |
return // stuff | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment