Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SSR] Rehydrating app contains the wrong classnames. #162

Open
pho3nixf1re opened this issue Jul 9, 2018 · 33 comments
Open

[SSR] Rehydrating app contains the wrong classnames. #162

pho3nixf1re opened this issue Jul 9, 2018 · 33 comments

Comments

@pho3nixf1re
Copy link

pho3nixf1re commented Jul 9, 2018

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;
@yocontra
Copy link
Owner

yocontra commented Jul 9, 2018

Sounds like this: facebook/react#10591

@yocontra
Copy link
Owner

yocontra commented Jul 9, 2018

If you intentionally need to render something different on the server and the client, you can do a two-pass rendering. Components that render something different on the client can read a state variable like this.state.isClient, which you can set to true in componentDidMount(). This way the initial render pass will render the same content as the server, avoiding mismatches, but an additional pass will happen synchronously right after hydration. Note that this approach will make your components slower because they have to render twice, so use it with caution.

@yocontra
Copy link
Owner

yocontra commented Jul 9, 2018

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.

@pho3nixf1re
Copy link
Author

pho3nixf1re commented Jul 9, 2018

I think providing a prop would be nice. There are times when it will be fine but this is an exception and the correct solution comes with complexity. The component in question here is way at the bottom of the page and will have a small amount of data on first load. Allowing opt-out with a clear documented warning is a great way to fix this simply and locally to the use case.

I'm also trying the redux solution and calling dispatch right after React.hydrate. If that works then I may just stick with that for now. My problem with that is it's heavy handed and required me to add more to my redux store for a very limited exceptional case. Most of my responsiveness is handled in CSS.

@pho3nixf1re
Copy link
Author

pho3nixf1re commented Jul 9, 2018

If you are open to the ssr flag then I'd be happy to try and implement it.

@yocontra
Copy link
Owner

yocontra commented Jul 9, 2018

@pho3nixf1re Even simpler would be if there was a lifecycle hook that got called on client render, and if ssr prop is present then run this.updateQuery(this.props)

Open to a PR - docs will need to be pretty good on it to not confuse people. This SSR stuff is always messy to document and test.

@dassennato
Copy link

dassennato commented Jul 25, 2018

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 facebook/react#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>
);

@fabiowallner
Copy link

fabiowallner commented Aug 7, 2018

I resolved it like this:

...

componentDidMount() {
  this.setState({client: true});
}

...

render() {
  let responsiveComponent = this.state.client ?
    (<MediaQuery minWidth={1280}>
        {matches => {
          if (matches) {
            return (
              <DesktopComponent/>
            );
          } else {
            return (
              <MobileComponent/>
            );
          }
        }}
      </MediaQuery>)
      : null;

  return(
    {responsiveComponent}
  )

}

Instead of not rendering the component server side at all you could render the mobile component for instance.

I hope it helps someone.

@albertstill
Copy link

albertstill commented Oct 24, 2018

react-responsive will need to change it's code so the first render pass on the client matches the server, which uses the values prop, regardless of the clients actual dimensions. Then the client code can check to see if the server render was different to the browsers actual dimensions. Then re-render if so. We did the same thing over here:

It's very important to realise a server client mismatch is dangerous when using hydrate in React 16, ReactDOM.hydrate can cause very strange html on the client if there is a mismatch. To mitigate this we use the two-pass rendering technique mentioned in the React docs. We render on the client in the first pass using values with css-mediaquery used on the server, then we use the browsers native window.matchMedia to get it's actual dimensions and render again if it causes different query results. This means there should be no React server/client mismatch warning in your console and you can safely use hydrate. As a result of above, if you are server side rendering and using ReactDOM.hydrate you must supply MediaQueryProvider a values prop.

Here is the code.

@darkowic
Copy link

darkowic commented Nov 14, 2018

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

@0x80
Copy link
Contributor

0x80 commented Jan 20, 2019

I think I just ran into this problem as well. In the component below I am rendering a grid of items which is 1 column on small screens and 2 on large screens. The SSR html contains the classname for small screen. I am trying to solve this by using the setState workaround, but somehow my approach doesn't trigger a re-render.

Any idea what I am missing here?

import React, { useState, useEffect } from "react";
import MediaQuery from "react-responsive";
import { Cell, Grid } from "styled-css-grid";
import Artist, { ArtistViewProps } from "./artist-view";

interface ArtistsGridProps {
  artists: ArtistViewProps[];
}

const ArtistsGrid: React.FunctionComponent<ArtistsGridProps> = ({
  artists
}) => {
  const [isClient, setIsClient] = useState(typeof window !== "undefined");

  useEffect(() => {
    setIsClient(true);
  }, []);

  const gridItems = artists.map(artist => (
    <Cell as="section" key={artist.name}>
      <Artist {...artist} />
    </Cell>
  ));

  return (
    <MediaQuery minWidth={900}>
      {matches => {
        console.log(
          "matches && isClient",
          matches && isClient,
          matches,
          isClient
        );

        return (
          <Grid
            css={`
              margin-bottom: 2em;
            `}
            columns={matches && isClient ? "360px 360px" : "minmax(1fr, 360px)"}
            gap="1em"
            rowGap="2em"
            justifyContent="space-around"
          >
            {gridItems}
          </Grid>
        );
      }}
    </MediaQuery>
  );
};

export default ArtistsGrid;

@0x80
Copy link
Contributor

0x80 commented Jan 20, 2019

I've now passed in isClient as a prop, since that's much simpeler. If I try to just split the rendering to SSR = mobile and client = responsive, I end up with messed up styling somehow. So for now I've just disabled SSR rendering all together by returning null.

I don't know what else to do. Well, I guess I can get rid of the styled-css-grid and implement the grid in pure css to avoid having to use react-responsive in the first place...

import React from "react";
import MediaQuery from "react-responsive";
import { Cell, Grid } from "styled-css-grid";
import Artist, { ArtistViewProps } from "./artist-view";

interface ArtistsGridProps {
  artists: ArtistViewProps[];
  isClient: boolean;
}

const ArtistsGrid: React.FunctionComponent<ArtistsGridProps> = ({
  artists,
  isClient
}) => {
  const gridItems = artists.map(artist => (
    <Cell as="section" key={artist.name}>
      <Artist {...artist} />
    </Cell>
  ));

  if (!isClient)
    // this works but disables SSR completely of course
    return null;

  // this results in messed up styling after rehydration
  /* return (
    <Grid
      css={`
        margin-bottom: 2em;
      `}
      columns={"minmax(1fr, 360px)"}
      gap="1em"
      rowGap="2em"
      justifyContent="space-around"
    >
      {gridItems}
    </Grid>
  ); */

  return (
    <MediaQuery minWidth={900}>
      {matches => {
        return (
          <Grid
            css={`
              margin-bottom: 2em;
            `}
            columns={matches ? "360px 360px" : "minmax(1fr, 360px)"}
            gap="1em"
            rowGap="2em"
            justifyContent="space-around"
          >
            {gridItems}
          </Grid>
        );
      }}
    </MediaQuery>
  );
};

export default ArtistsGrid;

@ghost
Copy link

ghost commented Nov 27, 2019

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;
}

@ghost
Copy link

ghost commented Nov 27, 2019

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

@nhuanhoangduc
Copy link

nhuanhoangduc commented Dec 8, 2019

Guys, this is how I solved ssr issue:
code

@yocontra
Copy link
Owner

yocontra commented Dec 9, 2019

This should be a pretty easy fix if somebody wants to tackle it, I'm swamped with work this week but if I have some spare time I will take it this weekend.

@coodoo
Copy link

coodoo commented Dec 26, 2019

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
})

@seb-thomas
Copy link

seb-thomas commented Feb 6, 2020

@coodoo Sorry where is isClient used? Is this meant to be used with other code?

const isClient = typeof window !== 'undefined'

@coodoo
Copy link

coodoo commented Feb 8, 2020

Nope isClient isn't used anywhere, edited it out :)

@jayarnielsen
Copy link

jayarnielsen commented Feb 27, 2020

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
Copy link

isotopeee commented Mar 9, 2020

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?

@team-gguys
Copy link

team-gguys commented Mar 9, 2020

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

@isotopeee
Copy link

isotopeee commented Mar 14, 2020

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

@team-gguys Thanks for your reply. But I'm not sure if that's correct.

Based on https://reactjs.org/docs/hooks-reference.html#timing-of-effects

Unlike componentDidMount and componentDidUpdate, the function passed to useEffect fires after layout and paint, during a deferred event. This makes it suitable for the many common side effects, like setting up subscriptions and event handlers, because most types of work shouldn’t block the browser from updating the screen.

In addition, from https://reactjs.org/docs/hooks-reference.html#uselayouteffect:

If you’re migrating code from a class component, note useLayoutEffect fires in the same phase as componentDidMount and componentDidUpdate. However, we recommend starting with useEffect first and only trying useLayoutEffect if that causes a problem.

@team-gguys
Copy link

team-gguys commented Mar 14, 2020

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

@jtomaszewski
Copy link

jtomaszewski commented Mar 14, 2020

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.

@jtomaszewski
Copy link

jtomaszewski commented Mar 14, 2020

One last thing to be discussed before doing the implementation is whether we want to use useLayoutEffect and useEffect. IMHO it should be useLayoutEffect, but unfortunately this will raise a warning (that does nothing bad) in the SSR logs:

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI.

I've been arguing here that there should be a way to call useLayoutEffect without triggering that warning, but since July 2019, nothing has moved into that direction, so we might need to:

@lucasriondel
Copy link

lucasriondel commented Apr 7, 2020

@jayarnielsen 's hook works perfectly. Thanks a lot.

@angiecortez
Copy link

angiecortez commented May 1, 2020

Guys, this is how I solved ssr issue:
code

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

@nhuanhoangduc
Copy link

nhuanhoangduc commented May 3, 2020

Guys, this is how I solved ssr issue:
code

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

My lady, if your aim is mobile-first, you should change those codes to

    isDesktop={isClient ? isDesktop : false},
    isMobile={isClient ? isMobile : true},

@angiecortez
Copy link

angiecortez commented May 4, 2020

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.

@igniteram
Copy link

igniteram commented Mar 6, 2021

Guys, fresnel addresses the very same issue. The conditional rendering in dom for responsive layouts is not the best way when it comes to SSR. As mentioned Artsy team there a couple of challenges -

It's impossible to reliably know the user's current breakpoint during the server render phase since that requires a browser.

Setting breakpoint sizes based on user-agent sniffing is prone to errors due the inability to precisely match device capabilities to size. One mobile device might have greater pixel density than another, a mobile device may fit multiple breakpoints when taking device orientation into consideration, and on desktop clients there is no way to know at all. The best devs can do is guess the current breakpoint and populate with assumed state.

I am currently using 'Fresnel' to solve this issue, until there is a solid need and solution provided by 'react-responsive'

@yocontra
Copy link
Owner

yocontra commented Mar 8, 2021

@igniteram Thanks for the link - I think for most basic cases that will work for people. Conditionally rendering is still ideal if you have large component trees being rendered inside media queries, but that project README explains the trade-offs pretty clearly.

@redbar0n
Copy link

redbar0n commented Dec 16, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests