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>
);
}
2
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.
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)
...
}
2