如何嵌套多个同类型React Context实现多层弹窗且不干扰外层上下文
问题说明
现有基于React Context API实现的弹窗状态管理代码如下:
export const ModalUIProvider: FC = ({ children }) => { const [isOpen, setIsOpen] = useState(false); const openModal = useCallback(() => setIsOpen(true), [setIsOpen]); const closeModal = useCallback(() => setIsOpen(false), [setIsOpen]); const toggleModal = useCallback( () => setIsOpen(!isOpen), [isOpen, setIsOpen] ); const modalContext = useMemo(() => { return { isModalOpen: isOpen, openModal, closeModal, toggleModal, }; }, [isOpen, openModal, closeModal, toggleModal]); return ( <ModalContext.Provider value={modalContext}> {children} </ModalContext.Provider> ); }; export const useModal = () => { return useContext<IModalContext>(ModalContext); };
根节点使用方式:
<ModalUIProvider> <ParentModal /> </ModalUIProvider>
业务侧调用Hook的方式:
const { isModalOpen, openModal, closeModal } = useModal();
当前业务需求:在已打开的弹窗内部放置按钮,点击按钮后在当前弹窗上层打开新弹窗,实现多层弹窗效果,且操作过程不干扰外层Context的已有状态。
实现方案
核心利用React Context就近匹配的特性:内层同名Context.Provider会覆盖外层提供的上下文值,不同层级Provider的状态完全独立,天然实现状态隔离。配合层级自动计算、Portal挂载即可实现无干扰的多层弹窗。
具体改造步骤
- 改造
ModalUIProvider,支持自动识别嵌套层级,计算对应弹窗的z-index,保证后打开的内层弹窗始终显示在上层 - 每个弹窗组件的内容外层都包裹独立的
ModalUIProvider,让弹窗内部的弹窗操作命中当前层级的独立上下文,不会修改外层状态 - 弹窗内容通过
ReactDOM.createPortal挂载到body节点,避免父容器的overflow、transform等属性影响弹窗显示
改造后的Provider代码
import { FC, useState, useCallback, useMemo, useContext, createContext } from 'react'; import { createPortal } from 'react-dom'; const BASE_Z_INDEX = 1000; const Z_INDEX_STEP = 10; // 补充Context类型定义 interface IModalContext { isModalOpen: boolean; openModal: () => void; closeModal: () => void; toggleModal: () => void; level: number; zIndex: number; } const ModalContext = createContext<IModalContext | null>(null); export const ModalUIProvider: FC = ({ children }) => { const [isOpen, setIsOpen] = useState(false); // 读取外层已有的弹窗上下文,计算当前层级 const outerContext = useContext(ModalContext); const currentLevel = outerContext ? outerContext.level + 1 : 0; const openModal = useCallback(() => setIsOpen(true), []); const closeModal = useCallback(() => setIsOpen(false), []); const toggleModal = useCallback(() => setIsOpen(prev => !prev), []); const modalContext = useMemo(() => { return { isModalOpen: isOpen, openModal, closeModal, toggleModal, level: currentLevel, zIndex: BASE_Z_INDEX + currentLevel * Z_INDEX_STEP }; }, [isOpen, openModal, closeModal, toggleModal, currentLevel]); return ( <ModalContext.Provider value={modalContext}> {children} </ModalContext.Provider> ); }; export const useModal = () => { const context = useContext<IModalContext>(ModalContext); if (!context) throw new Error('useModal must be used within ModalUIProvider'); return context; }; // 通用弹窗容器组件,自动处理Portal挂载 export const ModalContainer: FC<{ visible: boolean; zIndex: number; onClose: () => void; children: React.ReactNode }> = ({ visible, zIndex, onClose, children }) => { if (!visible) return null; return createPortal( <div className="modal-mask" style={{ zIndex, position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)' }} onClick={onClose} > <div className="modal-content" onClick={e => e.stopPropagation()}> {children} </div> </div>, document.body ) }
多层弹窗使用示例
// 第一层弹窗 const ParentModal = () => { const { isModalOpen, openModal, closeModal, zIndex } = useModal(); return ( <> <button onClick={openModal}>打开第一层弹窗</button> <ModalContainer visible={isModalOpen} zIndex={zIndex} onClose={closeModal}> <p>我是第一层弹窗</p> {/* 弹窗内部包裹独立Provider,隔离内层状态 */} <ModalUIProvider> <ChildModal /> </ModalUIProvider> <button onClick={closeModal}>关闭第一层弹窗</button> </ModalContainer> </> ) } // 第二层弹窗 const ChildModal = () => { // 此处Hook命中的是父弹窗内包裹的独立Provider,状态和外层完全隔离 const { isModalOpen, openModal, closeModal, zIndex } = useModal(); return ( <> <button onClick={openModal}>打开第二层弹窗</button> <ModalContainer visible={isModalOpen} zIndex={zIndex} onClose={closeModal}> <p>我是第二层弹窗</p> <button onClick={closeModal}>关闭第二层弹窗</button> </ModalContainer> </> ) } // 根节点使用 const App = () => { return ( <ModalUIProvider> <ParentModal /> </ModalUIProvider> ) }
该方案下每层弹窗维护自己的独立开关状态,内层弹窗的打开、关闭操作完全不会修改外层Context的状态,支持无限层级嵌套弹窗,自动处理层级顺序。
内容的提问来源于stack exchange,提问作者hucked
相关产品推荐
相关产品推荐

