如何解决Gatsby SSR后useState窗口尺寸导致的根页渲染问题
Gatsby SSR场景下窗口尺寸Hook首屏渲染异常问题
问题现象
在Gatsby项目中执行gatsby clean && gatsby build && gatsby serve做部署测试时,访问站点根路径(https://foobar.com/),所有依赖ThemeProvider获取尺寸参数的组件均无法正常渲染。经调试定位,问题根源为useState初始化时硬编码的窗口尺寸默认值为0,相关初始代码片段如下:
const [size, setSize] = useState({ windowWidth: hasWindow ? window.innerWidth : 0, windowHeight: hasWindow ? window.innerHeight : 0, })
相关问题代码
自定义窗口尺寸Hook完整实现
import { useState, useEffect } from 'react' const hasWindow = typeof window !== 'undefined' const useWindowDimensions = () => { const [size, setSize] = useState({ windowWidth: hasWindow ? window.innerWidth : 0, windowHeight: hasWindow ? window.innerHeight : 0, }) const updateSize = () => setSize({ windowWidth: window.innerWidth, windowHeight: window.innerHeight, }) useEffect(() => (window.onresize = updateSize), []) return size } export default useWindowDimensions
ThemeProvider上下文实现
Hook返回的尺寸值会直接传递给ThemeProvider上下文,相关代码如下:
import React, { createContext } from 'react' // Utils import useWindowDimensions from '../utils/useWindowDimensions' const defaultContext = { windowWidth: 0, windowHeight: 0, } export const ThemeContext = createContext(defaultContext) const ThemeProvider = ({ children }) => { const { windowWidth, windowHeight } = useWindowDimensions() return ( <ThemeContext.Provider value={{ windowWidth, windowHeight }}>{children}</ThemeContext.Provider> ) } export default ThemeProvider
已验证的无效方案
硬编码服务端fallback值为1600
对应修改代码:const [size, setSize] = useState({ windowWidth: hasWindow ? window.innerWidth : 1600, windowHeight: hasWindow ? window.innerHeight : 1600, })实际效果:桌面端浏览器可正常渲染,但移动端、窗口宽度小于1600px的场景下取值错误;只有跳转至站点其他路由触发
useState值更新后,渲染异常问题才会消失。移除window存在性判断直接读取属性
对应修改代码:const [size, setSize] = useState({ windowWidth: window.innerWidth, windowHeight: window.innerHeight, })实际效果:执行
gatsby build时会触发符合预期的服务端渲染报错:"window" is not available during server side rendering.
解决方案
问题核心原因是原Hook仅在窗口resize事件触发时才更新尺寸值,客户端hydration完成后没有主动同步真实窗口尺寸,导致首屏一直使用SSR阶段的初始默认值渲染。修复后的Hook实现如下:
import { useState, useEffect } from 'react' const hasWindow = typeof window !== 'undefined' const getInitialSize = () => ({ windowWidth: hasWindow ? window.innerWidth : 0, windowHeight: hasWindow ? window.innerHeight : 0, }) const useWindowDimensions = () => { const [size, setSize] = useState(getInitialSize) const updateSize = () => { if (!hasWindow) return setSize({ windowWidth: window.innerWidth, windowHeight: window.innerHeight, }) } useEffect(() => { // 客户端hydration完成后立刻同步一次真实窗口尺寸,解决首屏值错误问题 updateSize() // 改用标准事件监听绑定resize,避免覆盖全局其他resize回调 window.addEventListener('resize', updateSize) // 组件卸载时移除监听,避免内存泄漏 return () => window.removeEventListener('resize', updateSize) }, []) return size } export default useWindowDimensions
修复说明
- 核心逻辑:
useEffect内的代码仅在客户端执行,不会触发SSR报错。组件挂载后主动调用一次updateSize,会立刻读取真实窗口尺寸更新state,首屏hydration阶段就能拿到正确值,不需要等resize事件或路由跳转触发更新。 - 原代码直接给
window.onresize赋值的写法会覆盖全局其他位置绑定的resize事件回调,改用addEventListener/removeEventListener的标准写法更稳妥。 - 如果想减少hydration前后值差异导致的短暂布局抖动,可以把SSR阶段的初始fallback值改成项目受众最常用的设备尺寸(比如桌面端项目设为1920,移动端占比高设为375),这个值仅在SSR输出静态HTML、hydration完成前的极短时间生效,最终会被客户端读取到的真实值覆盖,不会出现硬编码固定值导致的小屏适配错误。
内容的提问来源于stack exchange,提问作者GʀᴜᴍᴘʏCᴀᴛ
相关产品推荐
相关产品推荐

