声明'use client'的React组件中window对象无法访问问题排查
我有一个显式声明了use client的客户端React组件:
"use client" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" import { useState, useTransition } from "react" import { useSearchParams } from "next/navigation" import { LoginSchema } from "@/schemas" import login from "@/app/actions/login" export default function LoginForm() { const currentURL = `/${window.location.href.split("/").slice(3).join("/")}` const searchParams = useSearchParams() const callbackUrl = searchParams.get("callback") || currentURL const [isPending, startTransition] = useTransition() const form = useForm<z.infer<typeof LoginSchema>>({ resolver: zodResolver(LoginSchema), defaultValues: { email: "", password: "", }, }) const onSubmit = (values: z.infer<typeof LoginSchema>) => { startTransition(() => { login(values, callbackUrl) .then((data) => { if (data?.error) { form.reset() setError(data.error) } if (data?.success) { form.reset() setSuccess(data.success) } if (data?.twoFactor) { setShowTwoFactor(true) } }) .catch(() => setError("Something went wrong")) }) } return ( <form className="space-y-6" onSubmit={form.handleSubmit(onSubmit)}> ... </form> ); }
当我尝试通过window.location.href获取URL时,出现如下错误:
ReferenceError: window is not defined at LoginForm (./src/components/auth/LoginForm.tsx:40:28)
我原本认为window对象仅在客户端可用,所以这样写应该没问题,请问我哪里出错了?
我已尝试的操作
我在函数内添加了console.log()来打印currentURL的计算值:
const currentURL = `/${window.location.href.split("/").slice(3).join("/")}` console.log(currentURL)
这能正常输出结果,说明组件显然可以访问window对象,但服务器仍抛出上述错误。我甚至不理解为什么服务器会报错,毕竟这个组件是在客户端渲染的!
问题根源
即使加了"use client",Next.js在服务器端渲染(SSR)或静态生成(SSG)阶段,还是会对客户端组件做初步的服务器端解析/渲染——这段代码会在服务器环境执行,而服务器没有浏览器的window对象,所以直接访问window会报错。你看到console.log正常输出是因为客户端运行时没问题,但服务器预渲染时已经触发了错误。
解决方法
1. 用useEffect延迟访问window(最稳妥的基础方案)
useEffect的回调只会在客户端组件挂载后执行,完全避开服务器端执行阶段:
"use client" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" import { useState, useTransition, useEffect } from "react" import { useSearchParams } from "next/navigation" import { LoginSchema } from "@/schemas" import login from "@/app/actions/login" export default function LoginForm() { const [currentURL, setCurrentURL] = useState("") const searchParams = useSearchParams() useEffect(() => { // 仅在客户端执行 const url = `/${window.location.href.split("/").slice(3).join("/")}` setCurrentURL(url) }, []) const callbackUrl = searchParams.get("callback") || currentURL // ... 其他代码保持不变 }
2. 直接检查window是否存在(简单场景快速修复)
在访问window前先判断环境,给服务器端一个默认值:
const currentURL = typeof window !== "undefined" ? `/${window.location.href.split("/").slice(3).join("/")}` : ""; // 服务器端时用空值或其他默认路径
3. 使用Next.js官方钩子usePathname(最推荐的最佳实践)
Next.js的next/navigation提供了usePathname钩子,专门用于获取当前路径,无需依赖window,且在服务器端和客户端都能正常工作:
"use client" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" import { useState, useTransition } from "react" import { useSearchParams, usePathname } from "next/navigation" // 新增usePathname import { LoginSchema } from "@/schemas" import login from "@/app/actions/login" export default function LoginForm() { const pathname = usePathname() // 获取当前路径 const searchParams = useSearchParams() const callbackUrl = searchParams.get("callback") || pathname // 直接替换成pathname // ... 其他代码保持不变 }
这个方法完全符合Next.js的设计逻辑,避免了直接操作浏览器API带来的环境兼容问题。
内容的提问来源于stack exchange,提问作者Ubiquitous Ubuntu

