Skip to content

Instantly share code, notes, and snippets.

@kylemh
Created April 2, 2024 16:55
Show Gist options
  • Save kylemh/d44f9033cff40bb32c3465730b6bfe07 to your computer and use it in GitHub Desktop.
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).
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