react-responsive: [SSR] Rehydrating app contains the wrong classnames.

I use the match render prop callback to set a prop which itself toggles a className. This renders fine on the server but on first load the client does not re-render to correctly display the className. I have even tried to for a state update using setState in an onChange callback provided to react-responsive. For some reason the matches values is correct when I examine the react tree but the DOM node is missing the correct class name. If I force the breakpoint by shrinking the window and expanding it again everything works correctly. I need that first render to be correct. The react-responsive-redux solution is not good for this use case as it tries to guess the viewport based on the user agent. I don’t need that, I just need the responsive component to re-render in the client. Is there something I am missing to accomplish this? Here is the component I’m trying to get to work:

import React from 'react';
import Responsive from 'react-responsive';
import BorderedRow from '../../../components/BorderedRow';
import styles from './index.scss';

const Posts = ({ posts, className }) => (
  <Responsive minWidth={981}>
    {match => (
      <div>
        {posts?.map?.(
          ({ id }) => (
            <BorderedRow stacked={match} key={id} className={styles.post}>
              <span>the rest of the content</span>
            </BorderedRow>
          )
        )}
      </div>
    )}
  </Responsive>
);

export default Posts;

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 14
  • Comments: 34 (9 by maintainers)

Most upvoted comments

the solution @nhuanhoangduc proposed works quite well. I modified it a bit to use hooks instead of hocs 🙂

import { useState, useLayoutEffect } from 'react';
import { useMediaQuery } from 'react-responsive';
import Theme from '../components/Theme';

const { breakpoint: breakpointMobile, breakpointTablet } = Theme;

function useResponsive() {
  const [isClient, setIsClient] = useState(false);

  const isMobile = useMediaQuery({
    maxWidth: breakpointMobile,
  });

  const isTablet = useMediaQuery({
    minWidth: breakpointMobile,
    maxWidth: breakpointTablet,
  });

  const isDesktop = useMediaQuery({
    minWidth: breakpointTablet,
  });

  useLayoutEffect(() => {
    if (typeof window !== 'undefined') setIsClient(true);
  }, []);

  return {
    isDesktop: isClient ? isDesktop : true,
    isTablet: isClient ? isTablet : false,
    isMobile: isClient ? isMobile : false,
  };
}

export default useResponsive;

@isotopeee I recommend using class component to understand deeply react lifecycle before moving to functional component and hook.

Guys, this is how I solved ssr issue: code

Incidentally, looks like Fresnel has a very different approach to solving this problem that might be preferable to some folks.

I’m facing the same issue. It turns out if we have things like this:

<Mobile>
    <div className="mobileComponent">
        Mobile Content
    </div>
</Mobile>
<Desktop>
    <div className="desktopComponent">
        Desktop Content
    </div>
</Desktop>

and if the client does not match with the server it ends up rendering at the DOM level this:

<!-- DOM -->
<div class="mobileComponent">
    Desktop Content
</div>

I tried the redux solution and to force a rerender on the client side, neither of two approaches worked. Looking at https://github.com/facebook/react/issues/10591 I realized that the issue only occurs when the main childrens of MediaQuery have similar HTML element but if another element is present in the middle it does not. So, for instance, this ugly workaround solves the issue:

<Mobile>
    <div className="mobileComponent">
        Mobile Content
    </div>
</Mobile>
<br style={{ display: 'none' }} /> {/* Ugly workaround */} 
<Desktop>
    <div className="desktopComponent">
        Desktop Content
    </div>
</Desktop>

Take a look at the <br/> I chose that element because if it’s placed a <div/> instead, or another common HTML tag, for this case the issue will be still present in a worse way:

<!-- DOM -->
<div style="display: 'none'" className="mobileComponent">
        Desktop Content
</div>

By the moment, until a better solution or a fix comes up, I’ll stick with the ugly workaround 😷

UPDATE: After some playground with the “ugly workaround” this way seems to work better:

const ResponsiveUglyWorkAround = (props) => (
    <React.Fragment>
        <br style={{ display: 'none' }} />
        <MediaQuery {...props} />
        <br style={{ display: 'none' }} />
    </React.Fragment>
);

For what it’s worth, here’s an example of a wrapper around useMediaQuery that fixes things for me. This still causes a flash of incorrect content if the server rendering doesn’t match the browser breakpoint, but at least rehydration will proceed correctly.

The hook allows you to specify what screen width the server should assume. It defaults to mobile since the consequences of “guessing” incorrectly are more severe for mobile, ie. if the server assumes desktop, you’ll get a much longer flash of incorrect content at 3G speeds than if it assumes mobile and rehydrates at broadband speeds.

// note that this might not be the technique to check if we're in the browser
// for all ssr environments, but it works for next!
const isBrowser = Boolean(process.browser);

export default function useBreakpoint(serverFallback = true) {
  const [browserFlushed, setBrowserFlushed] = useState(false);
  const isMobile = useMediaQuery({
    maxWidth: BREAKPOINT_WIDTH
  });
  useLayoutEffect(() => setBrowserFlushed(true), []);

  if (isBrowser && browserFlushed) {
    return isMobile;
  }
  return serverFallback;
}

Conditionally rendering is still ideal if you have large component trees being rendered inside media queries

I’d like to pose a question, based on a realization that escaped me for a while… Hoping that it may elucidate the matter for at least some of you that come across this issue. So please bear with me, even if it might question the fundamental approach this library has taken so far…:

Should you actually conditionally render large component trees? It amounts to showing different markup (HTML), which is, in effect: a different page. Even if ever so slightly. Whereas the web was built on the assumption that an url leads to a specific page. Not to a page that is able to morph into a completely different page (even if the ability is not abused to that extent).

Keeping this in mind, then it makes sense that media queries were made for conditionally applying styles to markup. Call it Classic Responsivity. Since the markup is intended to be the same, it went hand-in-hand with classic server-rendering. The way it was done was to render and ship all markup (all breakpoints), but show/hide markup by using CSS in the media queries. If you are willing to do this, and thus ship slightly redundant markup, it can still be done (with the caveat that you’d still get the side-effects from each component being rendered, even the ones not shown).

So, the problem with SSR might indicate that we’re fighting the web (…?) when we’re trying to use media queries to conditionally render markup this way. Call this desire Modern Responsivity. The window.screen or useWindowDimensions API in React Native for Web are actually much more suited for that. (necholas of React Native Web has already lamented how media queries are lacking wrt. responsivity) But these APIs only work client-side, since they inspect the browser window. Thus the pickle we are in with combining SSR + responsivity. Where Fresnel really seems the only viable (yet complex) alternative.

The Fresnel guys actually need some help with a useMedia hook if any of you fine folks would like to port your useMediaQuery hook there, to join efforts.

Some articles that might be relevant / helpful:

Hydrating text content from Server-Side Rendering which explains the React hydration warning you might have stumbled upon: Text content did not match. Server: "Count: 0" Client: "Count: "

How to combine React Native Web + responsivity + NextJS SSR, to get SEO, gives some ideas on various strategies to employ to get responsivity with SSR.

After tinkering around for a bit trying to solve the SSR issue with nextjs, I came up with following solution which works great on both client and server, just leave it here for people looking for solution in the future.

One nice thing about this approach is that it will re-detect the screen width after resizing, to the best of my knowledge that currently react-responsive didn’t support it yet.

const [isBig, setBig] = useState(false)

useEffect(() => {
	const foo = window.matchMedia("(min-width: 540px)")
	setBig(foo.matches)
	foo.onchange = evt => setBig(foo.matches)
	return () => foo.onchange = null
})

Hi, if somebody use CSS-in-JS like styled-components and Next.js, I implement a hook made by @jayarnielsen that is

import { useState, useLayoutEffect } from 'react';
import { useMediaQuery } from 'react-responsive';

const useResponsive = () => {
  const [isClient, setIsClient] = useState(false);

  const isMobile = useMediaQuery({
    maxWidth: 767
  });

  useLayoutEffect(() => {
    if (typeof window !== 'undefined') setIsClient(true);
  }, []);
  console.log('isClient', isClient);

  return {
    isMobile: isClient ? isMobile : false,
    isClient
  };
};

export default useResponsive;

import it in a page

import React from 'react';
import styled from 'styled-components';
import useResponsive from '../hocs/hocMobile';

const Text = styled.div`
  color: ${(props) => (props.isMobile ? 'red' : 'blue')};
`;
const Mobile = () => {
  const { isMobile, isClient } = useResponsive();

  if (!isClient) return <div>loading</div>;

  return <Text isMobile={isMobile}>hola</Text>;
};

export default Mobile;

I don´t know if I´m implementing good practices, but it works perfectly. I hope it works for you.

Guys, this is how I solved ssr issue: code

Hi, I use with styled-components, but when i reload, my mobile view, break

Support for SSR should be built-in out of the box in such a library. Currently, in case u want to use it in SSR+CSR enabled framework, like next.js, you basically have to wrap any useMediaQuery calls with your own hook/component, that does the isClient with useLayoutEffect thing.

Maybe I might make a PR for that in the next few days.

useLayoutEffect is the same as willMount or willReceiveProp, and useEffect is didMount or didUpdate, depends on your requiement

Tested @nhuanhoangduc’s solution and it worked. 🙌

However, I’ve used useEffect() instead of useLayoutEffect() and it’s working too.

Is there any reason why @nhuanhoangduc used useLayoutEffect() specifically?

@albertstill this is exactly what we did in our project

import React from 'react';
import PropTypes from 'prop-types';
import MediaQueryResponsive from 'react-responsive';

import withHasMounted from 'utils/withHasMounted';


const { Provider, Consumer } = React.createContext(undefined);

export const MediaContextProvider = Provider;

function MediaQuery({ hasMounted, ...props }) {
  if (process.env.IS_SERVER) {
    return (
      <Consumer>
        {(width) => <MediaQueryResponsive values={{ deviceWidth: width, width }} {...props} />}
      </Consumer>
    );
  }
  return (
    <MediaQueryResponsive
      values={hasMounted ? undefined : { deviceWidth: `${window.DEFAULT_SIZE}px`, width: `${window.DEFAULT_SIZE}px` }}
      {...props}
    />
  );
}

Note: the hasMounted comes from our HOC

On the server side we detect the device that send the request and we assume some DEFAULT_SIZE that we later add to the document for client. On client side we wait with first update to avoid react hydrating warning that content is different than server-side rendered and then we update the media query. Though it is not perfect. It adds 2 additional renders for each media query… I think we will decide in future to do not chech the hasMounted think and ignore the react hydrating warning (or disable it somehow).

This seems like a really common issue for people who use SSR so we might be able to incorporate a fix into react-responsive so people can quit the guessing stuff in the server-side.

Maybe if an ssr prop is present, we can do the double-render on the client even though this can cause substantial performance issues.