如何使用React Hook在Next.js SSR中检测窗口大小?

56

我正在使用 Next.js 和 react-dates 构建应用。

我有两个组件:DateRangePicker 组件和 DayPickerRangeController 组件。

当窗口宽度大于 1180 像素时,我希望渲染 DateRangePicker,如果小于此大小,则希望渲染 DayPickerRangeController

以下是代码:

      windowSize > 1180 ?
           <DateRangePicker
             startDatePlaceholderText="Start"
             startDate={startDate}
             startDateId="startDate"
             onDatesChange={handleOnDateChange}
             endDate={endDate}
             endDateId="endDate"
             focusedInput={focus}
             transitionDuration={0}
             onFocusChange={(focusedInput) => {
               if (!focusedInput) {
                 setFocus("startDate")
               } else {
                 setFocus(focusedInput)
                }
               }}
                /> :
             <DayPickerRangeController
               isOutsideRange={day => isInclusivelyBeforeDay(day, moment().add(-1, 'days'))}
               startDate={startDate}
               onDatesChange={handleOnDateChange}
               endDate={endDate}
               focusedInput={focus}
               onFocusChange={(focusedInput) => {
               if (!focusedInput) {
                 setFocus("startDate")
                 } else {
                  setFocus(focusedInput)
                 }
               }}
              /> 
          }

通常我使用React Hook与window对象一起使用来检测窗口的屏幕宽度,就像这个示例。

但是我发现,在服务器端渲染(SSR)时这种方法不可用,因为SSR渲染没有window对象。

有没有其他替代方法可以安全地获取窗口大小而不受SSR影响?

7个回答

122

你可以通过添加以下代码来避免在服务器端渲染(ssr)中调用检测函数:

// make sure your function is being called in client side only
if (typeof window !== 'undefined') {
  // detect window screen width function
}

来自您链接的完整示例:

import { useState, useEffect } from 'react';

// Usage
function App() {
  const size = useWindowSize();

  return (
    <div>
      {size.width}px / {size.height}px
    </div>
  );
}

// Hook
function useWindowSize() {
  // Initialize state with undefined width/height so server and client renders match
  // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
  const [windowSize, setWindowSize] = useState({
    width: undefined,
    height: undefined,
  });

  useEffect(() => {
    // only execute all the code below in client side
    // Handler to call on window resize
    function handleResize() {
      // Set window width/height to state
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
    
    // Add event listener
    window.addEventListener("resize", handleResize);
     
    // Call handler right away so state gets updated with initial window size
    handleResize();
    
    // Remove event listener on cleanup
    return () => window.removeEventListener("resize", handleResize);
  }, []); // Empty array ensures that effect is only run on mount
  return windowSize;
}

NB: 根据Sergey Dubovik的评论更新,我们不需要验证windows,因为useEffect在客户端运行。


14
如果我没记错的话,useEffect只在客户端运行,所以不需要使用typeof window !== 'undefined'的条件语句。 - Sergey Dubovik
1
如果您正在使用TypeScript编写此代码,则可能会遇到TS1251错误:在针对“ES3”或“ES5”时,不允许在块内声明函数。如果出现此错误,请将handleResize()函数移到if语句之外。 - matteo_dm
如果我没记错的话,这个解决方案在初始页面加载时无法工作,因为宽度和高度被设置为未定义。 - Javano Collins
我发现有必要在useEffect中直接调用setWindowSize,而不仅仅将其作为参数传递给window.addEventListener。在页面的初始加载中,“resize”事件监听器无法足够地填充窗口尺寸。 - N4v

38

虽然Darryl RN提供了一个绝对正确的答案,但我想做一个小备注:您实际上不需要在useEffect内部检查window对象是否存在,因为useEffect仅在客户端运行,从不在服务器端运行,而window对象始终在客户端可用。


1
我一直看到有人评论说useEffect不会在服务器端运行,但实际上它似乎是在运行。后来我注意到,我使用的代码将一个函数传递给了初始状态useState<T>(() => { dosomethingWithWindow}),这个函数被服务器端渲染器调用了。所以感谢提醒我这一点。 - Emile
2
这并不是真的,因为nextjs会对代码进行lint,并在变量未定义的情况下编译失败,并报错提示。 - monokrome
如果这个答案对你没有用,这里还有另一个选择:https://dev59.com/3b_qa4cB1Zd3GeqPSvGD#66696801 - Jacksonkr

6
useEffect(()=> {
   window.addEventListener('resize', ()=> {
       console.log(window.innerHeight, window.innerWidth)
   })
}, [])

0
你可以在这个页面上使用“use client”,将其视为客户端组件,因为它确实是客户端组件。 客户端组件 这样,你就可以避免由于服务器端渲染而导致的所有hack。

0

这是我正在使用的解决方案:一个小型的npm包,可以在此处找到use-window-size

一旦安装并导入,您所需要做的就是像这样使用它:

const { innerWidth, innerHeight, outerHeight, outerWidth } = useWindowSize();

return (
    <div>Window width: {innerWidth}</div>
)

0

由于某些原因,我一直在遇到错误,但这个方法对我有效。仅适用于宽度:

const useWidth = () => {
  const [width, setWidth] = useState(0)
  const handleResize = () => setWidth(window.innerWidth)
  useEffect(() => {
      handleResize()
      window.addEventListener('resize', handleResize)
      return () => window.removeEventListener('resize', handleResize)
      // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])
  return width
}

0
我喜欢Darryl RN的解决方案。如果你在页面加载时面临一个问题,即size.width最初是未定义的,只需简单地用window.innerWidth初始化状态。
const [windowSize, setWindowSize] = useState({
  width: window.innerWidth,
  height: window.innerHeight,
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接