Next.js 13+自定义_document.tsx创建与React Portal报错解决
Next.js 13+ 中 React Portal 的正确实现方案
问题根源
你把自定义_document.tsx放在src/app目录下是错误的——在Next.js 13+的App Router架构中,_document.tsx属于特殊文件,必须放在app目录的同级目录(比如src/_document.tsx或项目根目录下的_document.tsx),否则Next.js不会识别并加载这个文件,导致页面DOM中根本不存在#portal容器,触发Target container is not a DOM element错误。
方案一:通过_document.tsx预注入容器(推荐)
这种方式符合Next.js的SSR/SSG渲染逻辑,容器会在服务端就注入到DOM中,无需动态创建。
调整
_document.tsx的位置
将文件从src/app/_document.tsx移动到src/_document.tsx(使用src目录的项目),或直接放在项目根目录下(无src目录的项目)。保持
_document.tsx代码不变
import { Html, Head, Main, NextScript } from 'next/document' export default function Document() { return ( <Html> <Head /> <body> <Main /> <NextScript /> <div id='portal' /> </body> </Html> ) }
- 修复Modal组件的错误
修正选择器错误,优化客户端环境判断和元素获取逻辑:
import React, { ReactElement, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' type PropsType = { active: boolean setActive: (arg: boolean) => void children: ReactElement } function Modal({ active, setActive, children }: PropsType) { const portalRef = useRef<HTMLElement | null>(null) useEffect(() => { if (typeof window !== 'undefined') { portalRef.current = document.getElementById('portal') } }, []) // 容器不存在或模态框未激活时返回null if (!active || !portalRef.current || typeof window === 'undefined') { return null } return createPortal( <div className="modal active" onClick={() => setActive(false)} > <div className="modal__content active" onClick={e => e.stopPropagation()} > {children} </div> </div>, portalRef.current ) } export default Modal
方案二:动态创建/销毁Portal容器(无多余空DOM)
如果不想在DOM中常驻空容器,可以在Modal组件内动态创建容器,关闭模态框时自动销毁:
import React, { ReactElement, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' type PropsType = { active: boolean setActive: (arg: boolean) => void children: ReactElement } function Modal({ active, setActive, children }: PropsType) { const portalContainerRef = useRef<HTMLDivElement | null>(null) useEffect(() => { // 激活时动态创建容器并挂载到body if (typeof window !== 'undefined' && active && !portalContainerRef.current) { const container = document.createElement('div') container.id = 'portal' document.body.appendChild(container) portalContainerRef.current = container } // 组件卸载或模态框关闭时移除容器 return () => { if (portalContainerRef.current) { document.body.removeChild(portalContainerRef.current) portalContainerRef.current = null } } }, [active]) if (!active || !portalContainerRef.current || typeof window === 'undefined') { return null } return createPortal( <div className="modal active" onClick={() => setActive(false)} > <div className="modal__content active" onClick={e => e.stopPropagation()} > {children} </div> </div>, portalContainerRef.current ) } export default Modal
方案对比
- 方案一:容器常驻DOM,适合频繁打开的模态框,渲染逻辑更稳定;
- 方案二:仅在模态框激活时创建容器,DOM更干净,适合低频使用的模态框。
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

