在Next.js React应用程序中,窗口未定义

279

在我使用Next.js应用时,似乎无法访问window

未处理的拒绝(ReferenceError):window未定义

componentWillMount() {
    console.log('window.innerHeight', window.innerHeight);
}

Enter image description here


2
将代码移动到componentDidMount()中,该方法仅在客户端执行,因此可以使用window。此外,componentWillMount()在v17中已被弃用。https://github.com/zeit/next.js/wiki/FAQ#i-use-a-library-which-throws-window-is-undefined - Alexander Staroselsky
28个回答

1

0

您可以定义一个状态变量,并使用窗口事件处理来处理更改,如下所示。

const [height, setHeight] = useState();

useEffect(() => {
    if (!height) setHeight(window.innerHeight - 140);
    window.addEventListener("resize", () => {
        setHeight(window.innerHeight - 140);
    });
}, []);

0

对于那些无法使用钩子(例如函数组件)的人:

使用 setTimeout(() => yourFunctionWithWindow()); 将允许它获取窗口实例。猜测它只需要更多的时间来加载。


实际上,除了代码不好看,可能需要使用状态来使其工作之外,它对我来说并没有起作用,仍然报错“window未定义”。Node有自己的setTimeout定义,不知道window对象。 我通过使用'use client'并将this绑定到函数来解决了这个问题。 - undefined

0

这里没有一个答案是完全完整的,大多数人最终会来到这里,因为他们遇到了相同的问题,但是在不同的情况下需要不同的解决方案,所以让我分享一下从我的付费课程中提取的摘要。

原始问题提到了一个旧版本的React,使用回调函数,但是我的答案是基于hooks,并且与当前版本的React(16+)和Next(12+)更相关。

我的组件需要像Leaflet或jQuery这样的仅限浏览器的库

这些库在服务器端导入时会崩溃,因为它们期望window对象立即可用。

你需要我所称之为浏览器组件,这是一个无法在服务器上预渲染的React组件,与Next/React中在服务器上预渲染的客户端组件相反。

解决方案:使用next/dynamicssr: false进行延迟加载来导入你的浏览器组件。现在你的组件只会在浏览器中导入和运行。

我的组件需要使用window对象或其他仅限浏览器的代码进行渲染。遗憾的是,我无法修改它的代码。

这是一个浏览器组件。理想情况下,它应该被重写为客户端组件,但由于您无法控制代码,可能无法做到这一点。

您可以使用NoSsr组件。您还可以在父组件中使用useMounted hook(Next文档称之为"useClient"),但前提是父组件是客户端组件。因此,在这种情况下,NoSsr是更好的选择。

相关的实用代码:

export const useMounted = () => {
    const [mounted, setMounted] = useState<boolean>()
    // effects run only client-side
    // so we can detect when the component is hydrated/mounted
    // @see https://react.dev/reference/react/useEffect
    useEffect(() => {
        setMounted(true)
    }, [])
    return mounted
}

export function NoSsr({ children }: { children: React.ReactNode }) {
  const mounted = useMounted();
  if (!mounted) return null;
  return <>{children}</>;
}

我的组件需要使用window对象或其他仅限浏览器的代码进行渲染。我可以修改它的代码。

该组件应该是一个客户端组件。确保在服务器端预渲染时安全。

您可以使用上面的useMounted钩子重写组件并进行条件渲染:只有在组件挂载时才使用window对象进行渲染。

这将把您的“浏览器组件”转变为一个“客户端组件”,即使在服务器端不渲染任何内容或加载器,也是安全的。

我的组件需要一个可以在服务器端导入但不能在那里使用的库,比如D3。

更广泛地说,这是您想要对DOM进行命令式修改的情况,也是最接近原始问题的情况。

将其变为客户端组件。

您可以将有问题的代码包装在一个效果中,如下所示:useEffect(() => { console.log(window.innerHeight) }, [])。 在服务器端渲染期间,它将记录/渲染空内容或加载器,但这完全没问题。

我的组件代码完全相同,但在浏览器和服务器中的行为略有不同。

当使用JavaScript的日期或本地化功能时,会出现这种情况,你在客户端和服务器端使用相同的代码,但生成的结果却不同。
应该是一个客户端组件。
你可以使用supressHydrationWarning或者Suspense,就像this article中演示的那样。

0
这真让我抓狂,只有在useEffect内部定义函数才能正常工作。 但我不想在那里定义辅助函数,所以最后我是这样做的:
    'use client'
    const MyClientC = () => { 
    const desiredCalculation = helperFN.bind(this)
       ...
    }
    // helperFN.ts
    interface Window {
      innerHeight: number
      ...
    
    }
    const isBrowser = () => typeof window !== 'undefined';
    let Window: Window = {} as Window;
    if (typeof window !== 'undefined') {
      Window = window;
    }
    
    const helperFn = () => {     
        if (!isBrowser()) {
          return false;
        }
        return Window.innerHeight ...

也许这能帮到你

0

使用钩子的解决方案。 感谢 @luckgaer

当在客户端组件中调用时,此钩子在初始(服务器)渲染时将返回null,因此不会抛出上述错误。 我们只需在客户端组件中检查它不是null,并在第一次渲染时跳过使用它。以下是在客户端组件中使用该钩子的示例。

"use client";
import { useLayoutEffect, useState } from "react";

const useWindowWidth = (): number | null => {
  if (!global?.window) return null;
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  useLayoutEffect(() => {
    let timeoutId: NodeJS.Timeout;

    const handleResize = () => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        setWindowWidth(window.innerWidth);
      }, 100);
    };

    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return windowWidth;
};

export default useWindowWidth;

如何在客户端组件中使用:
const windowWidth = useWindowWidth();
if (windowWidth && windowWidth >= 850)...

-1

我想把这个有趣的方法留给未来的研究者。它使用了一个自定义钩子useEventListener,可以在许多其他需求中使用。

请注意,您需要对最初发布的内容进行一些小修改,就像我在这里建议的那样。

所以它会变成这样:

import { useRef, useEffect } from 'react'

export const useEventListener = (eventName, handler, element) => {
  const savedHandler = useRef()

  useEffect(() => {
    savedHandler.current = handler
  }, [handler])

  useEffect(() => {
    element = !element ? window : element
    const isSupported = element && element.addEventListener
    if (!isSupported) return

    const eventListener = (event) => savedHandler.current(event)

    element.addEventListener(eventName, eventListener)

    return () => {
      element.removeEventListener(eventName, eventListener)
    }
  }, [eventName, element])
}


-2

如果是 NextJS 应用程序并且在 _document.js 中,请使用以下代码:

<script dangerouslySetInnerHTML={{
        __html: `
            var innerHeight = window.innerHeight;
        `
        }} />

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