I am using Next.js 13 with the App Router and have the following client component, which uses media queries inside the javascript to display a sidebar differently for small/big screens.
"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>
)
}
Now, the site loads inside the client perfectly, but since the Next.js App Router renders this component once on the server and the server has no window
property, I will always get this error on the server (the console running npm run dev
in local development mode):
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);
I can replace the troublesome line with a if-else like this:
const [isLargeScreen, setIsLargeScreen] = useState(typeof window == "undefined" ? true : window.matchMedia("(min-width: 768px)").matches);
which then results in an runtime error on the client, if the server renders the component with the state set to true
, but the client (on a small screen in this example) renders the component with the state set to false
:
Unhandled Runtime Error
Error: Hydration failed because the initial UI does not match what was rendered on the server.
How can change this component so the server and client will not throw any errors?
I think you can use a trick like a "lazy hydration" in Next.js, there is severals methods, for example:
You can create a custom Hook, by creating a new file (useIsLargeScreen?)in the hooks folder :
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;
Than you use this hook on your Feed component:
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>
);
}
Another method I think about would be import dynamically your Sidebar, like this:
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>
);
}
there is also 3rd generic method to fix hydration error:
export default function Feed() {
const [ isMount, setIsMount ] = useState(false)
useEffect(() => {
setIsMount(true)
}, []);
// your code...
return isMount ? (
<div>
<Sidebar isLargeScreen={isLargeScreen} />
<div>...</div>
</div>
) : <div />
}