
在 Next.js 中直接通过 document.querySelector 获取 CSS Modules 生成的类名元素会失败,因为类名被哈希化;应改用 useRef 获取真实 DOM 节点,并注意服务端渲染(SSR)下 window 和 document 的不可用性。
在 next.js 中直接通过 `document.queryselector` 获取 css modules 生成的类名元素会失败,因为类名被哈希化;应改用 `useref` 获取真实 dom 节点,并注意服务端渲染(ssr)下 `window` 和 `document` 的不可用性。
在 Next.js 中使用 useEffect 操作 DOM 时,常见的错误包括:依赖未挂载的 DOM 节点、误用 CSS Modules 类名、忽略 SSR 环境限制。你遇到的 neonLine is null 错误,根本原因有两点:
CSS Modules 类名非原样输出
styles["nLine"] 实际生成的是类似 Neon_nLine__abc123 的唯一哈希类名,而非纯字符串 "nLine",因此 document.querySelector(".nLine") 必然返回 null。服务端无浏览器 API
Next.js 默认启用服务端渲染(SSR),而 useEffect 虽仅在客户端执行,但若代码中提前访问 window 或 document(如在 effect 外部),仍可能引发 hydration 错误或运行时异常。
✅ 正确做法是:用 useRef 绑定目标元素 + 条件检查 typeof window !== 'undefined'(可选,但推荐显式防护) + 避免重复添加/移除事件监听器。
以下是修复后的完整示例:
"use client"; // ⚠️ Next.js 13+ 必须声明为 Client Component
import React, { useState, useEffect, useRef } from "react";
import styles from "./Neon.module.css";
const Neon = () => {
const [scrolling, setScrolling] = useState(false);
const neonLineRef = useRef<htmldivelement>(null); // 类型安全的 ref
useEffect(() => {
// ✅ 安全访问 window(虽 useEffect 已保证客户端执行,但显式检查更健壮)
if (typeof window === "undefined") return;
const handleScroll = () => {
setScrolling(true);
const neonLine = neonLineRef.current;
if (!neonLine) return; // 防止 ref 未挂载
const scrollTop = window.scrollY;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const scrollPercentage = (scrollTop / (documentHeight - windowHeight)) * 100;
neonLine.style.height = `${scrollPercentage}%`;
};
// ✅ 使用节流优化滚动性能(避免高频触发)
let scrollTimer: NodeJS.Timeout;
const throttledScroll = () => {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(handleScroll, 16); // ~60fps
};
const checkScrollStop = () => {
if (scrolling) {
setScrolling(false);
} else {
// ✅ 仅在元素存在时设置 transition
if (neonLineRef.current) {
neonLineRef.current.style.transition = "height 0.5s ease-in-out";
}
}
};
window.addEventListener("scroll", throttledScroll);
window.addEventListener("scroll", checkScrollStop);
// ✅ 清理函数确保事件监听器被移除
return () => {
window.removeEventListener("scroll", throttledScroll);
window.removeEventListener("scroll", checkScrollStop);
clearTimeout(scrollTimer);
};
}, [scrolling]); // 依赖 scrolling 可选,但保持一致性更安全
return (
<div classname="{styles.con}">
<div classname="{styles.neon}"></div>
<div classname="{styles.line}">
{/* ✅ 使用 ref 绑定,不再依赖类名 */}
<div ref="{neonLineRef}" classname="{styles.nLine}"></div>
</div>
</div>
);
};
export default Neon;</htmldivelement>
? 关键注意事项总结:
- ✅ 始终为 useRef 添加泛型类型(如 useRef
(null)),提升类型安全性; - ✅ 在 useEffect 内部添加 if (!ref.current) return 防御性检查,避免空引用错误;
- ✅ 使用节流(throttle)或防抖(debounce)优化滚动事件性能,防止过度重绘;
- ✅ Next.js 13+ 中必须添加 "use client" 指令,明确标识该组件为客户端组件;
- ❌ 禁止在 useEffect 外访问 window/document,也避免在服务端环境(如 getServerSideProps)中调用浏览器 API;
- ? 若需在服务端预渲染部分样式,可结合 useState 初始化高度为 0%,再由 useEffect 动态更新,确保首屏一致性。
通过 useRef 替代 querySelector,不仅解决了类名哈希问题,更符合 React 的声明式设计哲学——让 DOM 引用成为组件状态的一部分,而非全局查询结果。










