You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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中,无需动态创建。

  1. 调整_document.tsx的位置
    将文件从src/app/_document.tsx移动到src/_document.tsx(使用src目录的项目),或直接放在项目根目录下(无src目录的项目)。

  2. 保持_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>
    )
}
  1. 修复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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 22:20:25