最近在做Nextjs项目,它有server component & client component。
- 默认行为:在
app
目录下,Next.js 默认将所有组件(如layout, page)视为服务端组件。 - 客户端组件:如果需要使用客户端功能(如
useState
、useEffect
),可以在组件顶部添加"use client"
指令,将其转换为客户端组件。 - 混合使用:可以在 Layout 中同时使用服务端和客户端组件,服务端组件用于初始渲染,客户端组件处理交互。
这时候如果你想在page里引用一个客户端组件的时候(如 Modal),就会报错:error ReferenceError: document is not defined
下面是我的例子以及解决方法
例子说明:用 Nextjs Intercepting Routes 拦截路由 /login,使LoginForm在dialog 中打开
1. src/app/@modal/(.)login/page.tsx
import { LoginForm } from "@/components/features/login/LoginForm";
import Modal from "@/components/ui/Modal";
// by default, the page is server component which will be compiled in server side, browser just run it
export default function LoginModal() {
return (
// Modal is client component which will be rendered in browser
<Modal>
<title>Please login</title>
<LoginForm />
</Modal>
);
}
2. src/app/login/page.tsx
import { LoginForm } from "@/components/features/login/LoginForm";
import { Box, Typography } from "@mui/material";
export default function LoginPage() {
return (
<Box>
<Typography id="modal-modƒal-title" variant="h6" component="h2">
Login Page
</Typography>
<LoginForm />
</Box>
);
}
3. src/components/ui/Modal.tsx (fix solution here)
"use client";
import { useRouter } from "next/navigation";
import React, { ElementRef, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
export default function Modal({ children }: { children: React.ReactNode }): React.ReactPortal | null {
// start: issue solution
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, [])
//end: issue solution
const router = useRouter();
const dialogRef = useRef<ElementRef<"dialog">>(null);
useEffect(() => {
if (!dialogRef.current?.open) {
dialogRef.current?.showModal();
}
}, []);
function onDismiss() {
router.back();
}
return isMounted ? createPortal(
<div className="modal-backdrop">
<dialog ref={dialogRef} className="modal" onClose={onDismiss}>
{children}
<button onClick={onDismiss} className="close-button" />
</dialog>
</div>,
document.getElementById("modal-root")!
) : null;
}
4. src/app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider, CssBaseline } from "@mui/material";
import theme from "@/theme";
import FullLayout from "@/components/layouts/FullLayout";
import ThemeProviderWrapper from "@/context/ThemeProviderWrapper";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Nextjs Sample",
description: "Generated by create next app",
};
export default function RootLayout({
children,
header,
footer,
modal
}: Readonly<{
children: React.ReactNode;
header: React.ReactNode;
footer: React.ReactNode;
modal: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<FullLayout>
<ThemeProviderWrapper>
<CssBaseline /> {/* Normalize CSS and apply baseline styles */}
{modal}
<div id="modal-root" />
{header}
<main>{children}</main>
{footer}
</ThemeProviderWrapper>
</FullLayout>
</body>
</html>
);
}