10

I'm using material-ui@v5, ie. the alpha branch.

Currently, I have a custom Timeline component which does this:

const CustomTimeline = () => {
  const mdDown = useMediaQuery(theme => theme.breakpoints.down("md"));

  return (
    <Timeline position={mdDown ? "right" : "alternate"}>
      {/* some children */}
    </Timeline>
  );
};

It works mostly as intended, but mobile users may experience layout shift because useMediaQuery is implemented using JS and is client-side only. I would like to seek a CSS implementation equivalence to the above code to work with SSR.

I have thought of the following:

const CustomTimeline = () => {

  return (
    <Fragment>
      <Timeline sx={{ display: { xs: "block", md: "none" } }} position="right">
        {/* some children */}
      </Timeline>
      <Timeline sx={{ display: { xs: "none", md: "block" } }} position="alternate">
        {/* some children */}
      </Timeline>
    </Fragment>
  );
};

This will work since the sx prop is converted into emotion styling and embedded in the HTML file, but this will increase the DOM size. Is there a better way to achieve that?

v1s10n_4
  • 598
  • 3
  • 18
Matthew Kwong
  • 2,548
  • 2
  • 11
  • 22
  • 1
    You can't do it with javascript since there's no window object in the node environment (during ssr). So css is the only option for sure. – Pieterjan May 13 '21 at 07:46
  • I think you misunderstood my question. The code snippets I attached are both working as expected. In fact, the first one is being used in my production site. What I'm looking for is the equivalent CSS implementation to enable better UX and SSR support. – Matthew Kwong May 13 '21 at 08:00
  • Yes I understand. But I'm affraid that the second code block is the only way to get around the `position` binding (which is javascript). The binding is evaluated by javascript, where you have the check for the window width. That can only work during client-side rendering, not during server-side rendering. So as you stated in your question the best option is to duplicate the element with the css-media-query check – Pieterjan May 13 '21 at 08:19
  • Although another option would be to find out which css rules are applied to the inner html elements of the Timeline component, add a dedicated css class to the Timeline components which you want to be responsive during csr as well as ssr, and write 2 responsive css blocks (one for timeline-small, one for timeline-large) containing this css. Then you got rid of your javascript bindings. – Pieterjan May 13 '21 at 08:38
  • 1
    Are you still looking for answers to this? – Tom Foster Nov 01 '21 at 14:27
  • Yeah, still using `useMediaQuery`. It works fine, but users with slow networks will experience layout shifts. – Matthew Kwong Nov 02 '21 at 01:14
  • What you receive from SSR is like a first render in HTML, cause the js is executed in the server side and it's running on node.js, it doesn't have your screen dimensions. It only generates HTML then sends it to you, so the only way is handling UI with conditional CSS. – Amir Gorji Nov 02 '21 at 08:01
  • I would like to point out that CSS is also client-side only so there's no way to make a CSS rule which affects the server-side render step as far as I know. The only thing you can do on the server side is user agent parsing to make an informed guess as to what the screen size *might* be based on whether the UA reports a mobile device or not and this will only work if you render the layout on the server on every request (which is usually how it works but not in all cases). – apokryfos Nov 03 '21 at 07:50

2 Answers2

2

I have experienced the same problem before and I was using Next.js to handle SSR. But it does not matter.

Please first install this package and import it on your root, like App.js

import mediaQuery from 'css-mediaquery';

Then, create this function to pass ThemeProvider of material-ui

  const ssrMatchMedia = useCallback(
        (query) => {
            const deviceType = parser(userAgent).device.type || 'desktop';
            return {
                matches: mediaQuery.match(query, {
                    width: deviceType === 'mobile' ? '0px' : '1024px'
                })
            };
        },
        [userAgent]
    );

You should pass the userAgent!

Then pass ssrMatchMedia to MuiUseMediaQuery

<ThemeProvider
                theme={{
                    ...theme,
                    props: {
                        ...theme.props,
                        MuiUseMediaQuery: {
                            ssrMatchMedia
                        }
                    }
                }}>

This should work. I am not using material-UI v5. Using the old one. MuiUseMediaQuery name might be changed but this approach avoid shifting for me. Let me know if it works.

oakar
  • 1,187
  • 2
  • 7
  • 21
  • I'm using Gatsby actually. So technically I don't have access to `req.headers` like the document suggests, will `navigator.userAgent` do the trick? – Matthew Kwong Nov 04 '21 at 04:59
  • If you render the page server-side, you have to get userAgent from ‘request.headers’. If you render only client-side, instead of the implementation that I suggest, use ‘const matches = useMediaQuery(…some-query, { noSsr: true })’ – oakar Nov 04 '21 at 07:23
0

To avoid first render before useMediaQuery launches From reactjs docs To fix this, either move that logic to useEffect (if it isn’t necessary for the first render), or delay showing that component until after the client renders (if the HTML looks broken until useLayoutEffect runs).

To exclude a component that needs layout effects from the server-rendered HTML, render it conditionally with showChild && and defer showing it with useEffect(() => { setShowChild(true); }, []). This way, the UI doesn’t appear broken before hydration.

Mohamed Daher
  • 609
  • 1
  • 10
  • 23