如何在React(Gatsby)中实现window滚动监听并解决生产环境报错
Hey there! Let's sort out that sticky header issue you're facing in Gatsby. Your current code works in development but breaks production for two key reasons:
- Gatsby uses server-side rendering (SSR) during builds, where the
windowobject doesn't exist. Directly referencing it throws an error when Gatsby generates your static pages. - Manipulating the DOM directly with
document.querySelectorconflicts with React's virtual DOM system, which can lead to unexpected behavior as your app updates.
Here's the proper React/Gatsby-friendly way to implement this feature:
Step-by-Step Breakdown
1. Use React State for Sticky Status
Instead of directly adding/removing classes from the DOM, we'll use useState to track whether the header should be sticky. This keeps our UI in sync with React's data flow.
2. Safely Handle Event Listeners with useEffect
useEffect lets us add scroll listeners when the component mounts, and clean them up when it unmounts. We'll also add guards to avoid SSR errors by checking if window exists first.
3. Use React Refs for DOM Access
Instead of hunting for elements with document.querySelector, we'll use useRef to get a direct, React-approved reference to our nav element.
4. Optional (But Recommended): Throttle Scroll Events
Scroll events fire constantly—adding a throttle limits how often our check function runs, which improves performance.
Full Working Code Example
import { useState, useEffect, useRef } from 'react'; import headerStyles from './your-header-styles.module.css'; // Update this path to your styles const Header = () => { // State to track if header should be sticky const [isSticky, setIsSticky] = useState(false); // Ref to access the nav DOM element const navRef = useRef(null); // Throttle function to limit scroll event frequency const throttle = (func, delay) => { let lastExecution = 0; return (...args) => { const now = Date.now(); if (now - lastExecution >= delay) { func(...args); lastExecution = now; } }; }; const checkScrollPosition = () => { // Guard clause to avoid SSR errors if (typeof window === 'undefined') return; const viewportWidth = window.innerWidth || document.documentElement.clientWidth; if (viewportWidth > 1100) { const scrollPosition = Math.round(window.scrollY); // Update state based on scroll position setIsSticky(scrollPosition > 100); } else { // Reset sticky state on smaller screens setIsSticky(false); } }; useEffect(() => { // Create a throttled version of our check function const throttledCheck = throttle(checkScrollPosition, 100); // Add scroll listener only if window exists if (typeof window !== 'undefined') { window.addEventListener('scroll', throttledCheck); // Run once on mount to set initial state checkScrollPosition(); } // Cleanup: Remove listener when component unmounts return () => { if (typeof window !== 'undefined') { window.removeEventListener('scroll', throttledCheck); } }; }, []); // Empty dependency array = runs once on mount, cleanup on unmount return ( <nav ref={navRef} id="nav" className={`${isSticky ? headerStyles.sticky : ''}`} > {/* Add your navigation content here */} </nav> ); }; export default Header;
Key Improvements Explained
- SSR Safety: Every time we reference
window, we check if it exists first—this prevents errors during Gatsby's build process. - React State: The
isStickystate controls the header's class, so React handles updating the DOM correctly. - Refs:
navRefgives us a safe way to access the nav element without querying the entire document. - Throttling: The throttle function ensures our check runs at most once every 100ms, reducing unnecessary computations.
- Cleanup: The
useEffectcleanup function removes the scroll listener when the component unmounts, preventing memory leaks.
This approach will work seamlessly in both development and production environments, and aligns with React's best practices.
内容的提问来源于stack exchange,提问作者JFelix

