yocontra / react-responsive

CSS media queries in react - for responsive design, and more.
https://contra.io/react-responsive
MIT License
6.95k stars 297 forks source link

Hydration failed because the initial UI does not match what was rendered on the server. #298

Closed aureliolk closed 1 year ago

aureliolk commented 1 year ago

Hi guys ! I have this error and I can't solve it.. Can anyone help me?

image

image

natrocore commented 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;
traviswimer commented 1 year ago

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

dengue8830 commented 1 year ago

This is a classic problem with SSR. I found 3 solutions here:

  1. Use @artsy/fresnel. It removes the unwanted code while keeping the hydration consistent. The downside is: it actually doesn't support react 18.
  2. Use 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.
  3. Accept the error message and hope that react does a full render when a mismatch is found. I read the docs and they doesn't specify what happens when a big mismatch is found and that could lead to serious mixed css bugs.
henrybabbage commented 1 year ago

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>
        )
    }
satvik007 commented 1 year ago

@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/

henrybabbage commented 1 year ago

@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.

yocontra commented 1 year ago

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.