Error: useMediaQuery is a client-only hook
Asked Answered
L

3

10

I am trying to to create an app using NextJS 13.4.7 and am using useMediaQuery hook to detect if the view is mobile or not. My component is not a server component, but I keep getting client-only hook errors. Now my client component is a part of server component but from the documentation it seems that it is allowed. Am I missing something? Below is the code,

"use client";

import { useContext, useState, useEffect } from "react";
import PageDataContext from "@/contexts/PageDataContext";
import useMediaQuery from "@/hooks/useMediaQuery";
import { motion } from "framer-motion";

export default function Header() {
  const content = useContext(PageDataContext);
  const isSmallDevice = useMediaQuery("only screen and (max-width : 991px)");
  const [hideNav, setHideNav] = useState(isSmallDevice);
  const { home: homeContent } = content;
  const { navigation: navigationContent } = content;
  const { name } = homeContent;
  const [firstName, lastName] = name.split(" ");

  const onTheRight = { x: "100%" };
  const inTheCenter = { x: 0 };
  const onTheLeft = { x: "-100%" };

  const transition = { duration: 0.6, ease: "easeInOut" };

  const variants = {
    open: { opacity: 1, x: "0%" },
    closed: { opacity: 0, x: "100%" },
  };
  const toggleSideNav = () => {
    setHideNav(!hideNav);
  };

  useEffect(() => {
    setHideNav(isSmallDevice);
  }, [isSmallDevice]);

  return (
    <header id="site_header" className="header">
      <div className="header-content clearfix">
        <div className="text-logo">
          <a href="/">
            <div className="logo-symbol">{firstName[0].toUpperCase()}</div>
            <div className="logo-text">
              {firstName} <span>{lastName}</span>
            </div>
          </a>
        </div>
        <motion.div
          animate={hideNav ? "closed" : "open"}
          variants={variants}
          style={{
            position: "fixed",
            width: "100%",
            height: "calc(100% - 52px)",
            top: "52px",
            left: "auto",
          }}
        >
          <div className="site-nav">
            <ul className="leven-classic-menu site-main-menu">
              {navigationContent.map((item, idx) => (
                <li className="menu-item" key={"header_key_" + idx}>
                  <a href={item.url}>{item.title}</a>
                </li>
              ))}
            </ul>
          </div>
        </motion.div>
        <a className="menu-toggle mobile-visible" onClick={toggleSideNav}>
          <i className="fa fa-bars"></i>
        </a>
      </div>
    </header>
  );
}
Levity answered 3/7, 2023 at 22:48 Comment(2)
could you share the error message?Lonergan
Did you end up solving this issue?Pasty
C
0

I encountered the same problem. This is because useMediaQuery renders both server-side and client-side. Even if you give the "use client" directive, if it somehow works where ssr is, this error occurs.

As a solution, you can create a custom hook and use this hook by dynamic import:

example custom hook: import useMediaQuery from "@/hooks/useMediaQuery";

const useDevice = () => {
  const isMobile = useMediaQuery("only screen and (max-width : 767px)");
  const isTablet = useMediaQuery(
    "only screen and (min-width : 768px) and (max-width : 1024px)"
  );
  const isDesktop = useMediaQuery(
    "only screen and (min-width : 1025px) and (max-width : 2379px)"
  );
  const isDesktopLarge = useMediaQuery("only screen and (min-width : 2380px)");

  return { isMobile, isTablet, isDesktop, isDesktopLarge };
};

export default useDevice;

usage:

"use client";
const useDevice = dynamic(() => import("@/app/hooks/useDevice"), {
  ssr: false
});


export default function Header() {
  const device = useDevice();
  console.log(device?.isDesktop)

  ...

}
Clipping answered 18/12, 2023 at 10:15 Comment(2)
Hi, use client has no effect inside hooks file like you do.Lipase
Hi, @Lipase , than you for review. You're right. I need remove "use client" inside hook file. I Fixed.Clipping
L
0

It is recommended to separate your client component from your server component in the app directory.

By default, a page is a server component.

From what I see in your code, you have opted for "use client", but it would be best for you to create a separate component that includes this code.


By the way, the issue lies within your useMediaQuery hook. I cannot see the code, but I believe it is not SSR-friendly.

Here is an implementation of the useMediaQuery hook:

import { useState } from 'react'
import { useIsomorphicLayoutEffect } from 'usehooks-ts'

type UseMediaQueryOptions = {
  defaultValue?: boolean
  initializeWithValue?: boolean
}

const IS_SERVER = typeof window === 'undefined';

export function useMediaQuery(
  query: string,
  {
    defaultValue = false,
    initializeWithValue = true,
  }: UseMediaQueryOptions = {},
): boolean {
  const getMatches = (query: string): boolean => {
    if (IS_SERVER) {
      return defaultValue
    }
    return window.matchMedia(query).matches
  }

  const [matches, setMatches] = useState<boolean>(() => {
    if (initializeWithValue) {
      return getMatches(query)
    }
    return defaultValue
  })

  function handleChange() {
    setMatches(getMatches(query))
  }

  useIsomorphicLayoutEffect(() => {
    const matchMedia = window.matchMedia(query)

    handleChange()

    if (matchMedia.addListener) {
      matchMedia.addListener(handleChange)
    } else {
      matchMedia.addEventListener('change', handleChange)
    }

    return () => {
      if (matchMedia.removeListener) {
        matchMedia.removeListener(handleChange)
      } else {
        matchMedia.removeEventListener('change', handleChange)
      }
    }
  }, [query])

  return matches
}

From: https://usehooks-ts.com/react-hook/use-media-query

The hook will not throw any errors.

Note: You do not need to use dynamic import to use hooks. You just need to make the hook SSR-friendly by avoiding the use of window or document before the app is on the client.

Lipase answered 23/3 at 3:0 Comment(0)
S
0

@Gaurang you can use this simple custom hook

import * as React from "react";

export function useMediaQuery(query: string) {
  const [value, setValue] = React.useState(false);

  React.useEffect(() => {
    function onChange(event: MediaQueryListEvent) {
      setValue(event.matches);
    }

    const result = matchMedia(query);
    result.addEventListener("change", onChange);
    setValue(result.matches);

    return () => result.removeEventListener("change", onChange);
  }, [query]);

  return value;
}

usage

const isSmallDevice = useMediaQuery("only screen and (max-width : 768px)");
Scantling answered 25/4 at 20:27 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Hestia

© 2022 - 2024 — McMap. All rights reserved.