Closed aureliolk closed 1 year ago
@aureliolk try to do this
import { useEffect, useState } from "react";
import { useMediaQuery } from "react-responsive";
type HeaderProps = {
className?: string;
};
const Header = ({ className }: HeaderProps) => {
const [mounted, setMounted] = useState(false);
const isDesktop = useMediaQuery({ query: "(min-width: 767px)" });
useEffect(() => {
setMounted(true);
}, []);
return (
<>
{mounted && (
<header>
{isDesktop && <h1>{isDesktop && "Desktop"}</h1>}
</header>
)}
</>
);
};
export default Header;
This error is caused by the HTML file received from the server not matching what is generated by your client-side React app.
In the context of react-responsive
, this is most likely because the pre-rendered server HTML is using a different breakpoint than when you load your app on your actual device.
The solution suggested by @natrocore forces both the server and client to NOT render the header during the first render. This allows the HTML to match, since it is blank in both cases.
Of course, by doing this you may be defeating the purpose of having server-side rendering in the first place, since nothing meaningful is actually being rendered by the server.
The docs suggest a different approach:
At times you may need to render components with different device settings than what gets automatically detected. This is especially useful in a Node environment where these settings can't be detected (SSR) or for testing.
https://github.com/yocontra/react-responsive#forcing-a-device-with-the-device-prop
In other words, you'd want something more like:
export function Headers({ children }: React.PropsWithChildren) {
const [hydrated, setHydrated] = useState(false);
const isDesktop = useMediaQuery(
{ query: "(min-width: 414px)" },
hydrated ? {} : { deviceWidth: 1600 }
);
useEffect(() => {
setHydrated(true);
}, []);
return (
<>
{isDesktop && (
<header>
<h1>Desktop</h1>
</header>
)}
</>
);
}
You can also simplify this by using an NPM package I published: react-hydration-provider
import React from "react";
import { useMediaQuery } from "react-responsive";
import { useComponentHydrated } from "react-hydration-provider";
export function Headers({ children }: React.PropsWithChildren) {
const hydrated = useComponentHydrated();
const isDesktop = useMediaQuery(
{ query: "(min-width: 414px)" },
hydrated ? {} : { deviceWidth: 1600 }
);
return (
<>
{isDesktop && (
<header>
<h1>Desktop</h1>
</header>
)}
</>
);
}
The best option is probably to use react-responsive
's ResponsiveContext.Provider
to ensure all components initially load the same as the server: https://github.com/yocontra/react-responsive#supplying-through-context
In other words, you could keep your Headers
component unchanged and put something like this at the top level of your app:
import React from "react";
import { Context as ResponsiveContext } from "react-responsive";
import { useComponentHydrated } from "react-hydration-provider";
import Headers from "./Headers.tsx";
export function App() {
const hydrated = useComponentHydrated();
return (
<ResponsiveContext.Provider value={hydrated ? {} : { width: 1600 }}>
<Headers />
</ResponsiveContext.Provider>
);
}
If you'd like to read more about hydrations errors and how to fix them, I wrote a blog post about it: Easily Fix React Hydration Errors
This is a classic problem with SSR. I found 3 solutions here:
render
instead hydrate
. On this one the user will still receive and see the server rendered html and styles but you will lose the performance boost of hydrating over rendering from scratch. Tbh I'm not sure if that performance boost worth it, we should check benchmarks but if the react team built it is for some good reason.Hi @traviswimer thanks so much for your detailed explanation and suggestions. Super useful. I am now attempting this method but with both hydrated added to the useMediaQuery object, and the other method using react-responsive's ResponsiveContext.Provider I am getting a blank screen when I load my site. Wondering if there is an obvious pitfall I am missing (speaking as a someone very new to React and programming in general).
Simplified version of my code looks like so:
export default function IndexPage() {
const hydrated = useComponentHydrated()
const isTabletOrMobile = useMediaQuery(
{ query: '(max-width: 1224px)' },
hydrated ? {} : { deviceWidth: 1224 }
)
const isDesktopOrLaptop = useMediaQuery(
{ query: '(min-width: 1224px)' },
hydrated ? {} : { deviceWidth: 1224 }
)
return (
<div>
{isDesktopOrLaptop && <div>Desktop Content</div>}
{isTabletOrMobile && <div>TabletOrMobile Content</div>}
</div>
)
}
@henrybabbage use undefined instead of {}
const isTabletOrMobile = useMediaQuery(
{ query: '(max-width: 1224px)' },
hydrated ? undefined : { deviceWidth: 1224 }
)
You actually don't need to make 2 queries.
return (
<div>
{isDesktopOrLaptop && <div>Desktop Content</div>}
{!isDesktopOrLaptop && <div>TabletOrMobile Content</div>}
</div>
)
Also its better to set {width: 1224}
instead of {deviceWidth: 1224}
. Setting deviceWidth doesn't work as expected for me when the media query is about width. For width vs device-width - https://www.sitepoint.com/media-queries-width-vs-device-width/
@traviswimer Thank you for the kind reply, that did it! Appreciate the pointer on only needing a single query, and use of width over deviceWidth too, noted. Many thanks again.
I've added some more docs specifically for next.js SSR - it seems like there are enough workarounds here as well for other approaches, so I'm going to close this for now. If anyone has any suggestions for the docs, or ways this library can support SSR better please open a new issue.
Hi guys ! I have this error and I can't solve it.. Can anyone help me?