如何在Next.js应用路由器中使用媒体查询?

svmlkihl  于 2023-08-04  发布在  其他
关注(0)|答案(2)|浏览(116)

我正在使用Next.js 13与应用路由器,并有以下客户端组件,它使用JavaScript中的媒体查询来显示小/大屏幕的侧边栏。

"use client";

export default function Feed() {
    const [isLargeScreen, setIsLargeScreen] = useState(window.matchMedia("(min-width: 768px)").matches);

    useEffect(() => {
    window
        .matchMedia("(min-width: 1024px)")
        .addEventListener('change', e => setIsLargeScreen(e.matches));
    }, []);

    return (
        <div>
            <Sidebar isLargeScreen={isLargeScreen}/>
            <div>...</div>
        </div>
    )
}

字符串
现在,网站在客户端内完美加载,但由于Next.js应用路由器在服务器上呈现此组件一次,并且服务器没有window属性,因此我将始终在服务器上获得此错误(在本地开发模式下运行npm run dev的控制台):

error ReferenceError: window is not defined
at Feed (./app/feed/page.tsx:32:95)
> 17 |     const [isLargeScreen, setIsLargeScreen] = useState(window.matchMedia("(min-width: 768px)").matches);


我可以用如下的if-else替换这条麻烦的代码行:

const [isLargeScreen, setIsLargeScreen] = useState(typeof window == "undefined" ? true : window.matchMedia("(min-width: 768px)").matches);


如果服务器呈现状态设置为true的组件,而客户端(在本例中在小屏幕上)呈现状态设置为false的组件,则会导致客户端上的运行时错误:

Unhandled Runtime Error

Error: Hydration failed because the initial UI does not match what was rendered on the server.


如何更改此组件,使服务器和客户端不会抛出任何错误?

idv4meu8

idv4meu81#

我认为你可以在Next.js中使用类似“懒惰补水”的技巧,有几种方法,例如:
您可以通过创建一个新文件(useIsLargeScreen?)在hooks文件夹中:

function useIsLargeScreen() {

  const [isLargeScreen, setIsLargeScreen] = useState(false); 

  useEffect(() => {
    setIsLargeScreen(window.matchMedia("(min-width: 768px)").matches);

    // I write this into a function for better visibility
    const handleResize = (e) => {
      setIsLargeScreen(e.matches);
    };

    const mediaQuery = window.matchMedia("(min-width: 1024px)");

    mediaQuery.addEventListener('change', handleResize);

    // Clean up the event listener when the component unmounts
    return () => {
      mediaQuery.removeEventListener('change', handleResize);
    };
  }, []);

  return {
    isLargeScreen
  }
};

export default useIsLargeScreen;

字符串
然后在Feed组件上使用此钩子:

export default function Feed() {
  // import this hook into this component
  const {isLargeScreen} = useIsLargeScreen();

  // maybe without conditional check if you want to render this on smaller screen with different style 
  return (
    <div>
      {isLargeScreen && <Sidebar isLargeScreen={isLargeScreen} />}
      <div>...</div>
    </div>
  );
}


另一种方法是动态导入你的侧边栏,像这样:

import dynamic from "next/dynamic";
    
    const Sidebar = dynamic(()=> import("../path/to/Sidebar"), {  //put your Sidebar component path
      ssr: false,
    })

    export default function Feed() {
    // your code...

    return (
      <div>
        <Sidebar isLargeScreen={isLargeScreen} />
        <div>...</div>
      </div>
    );
  }


还有第三种通用方法来修复水合误差:

export default function Feed() {

      const [ isMount, setIsMount ] = useState(false)

      useEffect(() => {
        setIsMount(true)
      }, []);

    // your code...

     return isMount ? (
       <div>
         <Sidebar isLargeScreen={isLargeScreen} />
         <div>...</div>
       </div>
     ) : <div />
   }

63lcw9qa

63lcw9qa2#

我已经看到,有时候在客户端组件中获取窗口对象需要时间,而且通常需要进行递归检查。

"use client";

export default function Feed() {
  const [isLargeScreen, setIsLargeScreen] = useState(false) //can't use window here

  const addListener = () => {
    if (window) {
      // Do whatever you need with window here...
      window
        .matchMedia("(min-width: 1024px)")
        .addEventListener('change', e => setIsLargeScreen(e.matches))
    } else {
      setTimeout(addListener, 100)
    }
  }

  useEffect(() => {
    addListener()
  }, [])

  return (
    <div>
      <Sidebar isLargeScreen={isLargeScreen}/>
      <div>...</div>
    </div>
  )
}

字符串

相关问题