5

I'm using material-ui with SSR. I've set up the SSR machinery on my app according to the instructions on the material-ui docs. It does work, but not without a rendering issue that so far has been very hard to debug. More details follow.

SSR + loading state (which causes the comp. in question to not render in one of the SSR rendering passes, more on that below) cause inconsistent ID in the className of a specific component that renders on the second SSR rendering pass but not on the first (because its rendering is conditioned to having the data available).

This causes the markup sent from the server to have a different CSS class name for this component, causing a visual inconsistency in when hydration happens, as you may see below:

SSRed component:

enter image description here

Hydrated component:

Hydrated component

The actual class available in the DOM is:

.PrivateSwitchBase-input-393 {
  top: 0;
  left: 0;
  width: 100%;
  cursor: inherit;
  height: 100%;
  margin: 0;
  opacity: 0;
  padding: 0;
  z-index: 1;
  position: absolute;
}

But because of the CSS class name mismatch, an inexistent class PrivateSwitchBase-input-411 is applied to the CheckBox input, and it's not made invisible, as it should be, resulting in the visual glitch upon hydration in the client-side.

And I get the following warning from React:

Warning: Prop className did not match. Server: "PrivateSwitchBase-input-411" Client: "PrivateSwitchBase-input-393".

I'd expect the className to match and the component rendering to be the same in both the server and the client.

Steps to Reproduce

I have a TodoItem component:

import React from 'react';
import { 
  FormControlLabel,
  Checkbox
} from '@material-ui/core';

const TodoItem = (props) => {
  return (
    <FormControlLabel style={props.style} control={<Checkbox/>} label={props.title} />
  )
}

export default TodoItem;

And a Todos component (simplified version):

import React from 'react';
import  SortableTree, { getFlatDataFromTree } from '../lib/sortable-tree';
import { observer } from "mobx-react";
import { useQuery } from '../models/reactUtils';
import { Paper } from '@material-ui/core';

const Todos = observer((props) => {
  const {store, loading} = useQuery(store => store.fetchActiveTodoTree());

  return (
    <>
      <Paper style={{padding: '20px'}}>
        <SortableTree
          treeData={store.activeTodoTree.toJSON()}
          generateNodeProps={({node, path}) => ({
            title: (
              <TodoItem title={node.title} />
            ),
          })}
        />
      </Paper>
  )
});

I load the app that renders the Todos component. This component loads some data from the backend API using mst-gql and passes over to the SortableTree component;

When running from the server, I use the getDataFromTree function from mst-gql to wait for the data promises to be resolved and finally get the HTML to be sent back to the client (I've omitted this code from here, but can share it if needed. It looks like the one here, just that my version uses mst-gql instead of Redux) . Note that the component tree needs to be rendered twice:

  1. The first time to trigger any data fetching promises;

  2. Then once these promises are resolved, the last pass is done to render the tree with the data that became available.

After the markup from the server is sent to the client, then React.hydrate takes place. That's when the component in question is then rendered with the visible input because of the inexistent CSS class.

I'm convinced the problem happens because of point 2 above. The first time the Todos component is rendered, the store.activeTodoTree data is not yet available, so the SortableTree component doesn't render anything, hence the TodoItem component that's supposed to be used inline by the SortableTree as its tree nodes (refer to the screenshots above) is not rendered the first time (but everything else is). I don't know exactly how the className ID suffix generation logic works in MUI, but because of this, the suffix for the PrivateSwitchBase-input class (used for MUI's CheckBox component's internal checkbox input) has a mismatch of IDs between the server and the client, causing the visual glitch I've shown in the screenshots above.

One interesting thing though, is that the child nodes of the Foobar node, all render as expected even after hydration, as you may see below:

Child tree nodes don't suffer from the SSR rendering glitch when hydration takes place

You can see that the checkbox input for these nodes are hidden, which means the CSS class was correctly applied. I have no idea why that only happens to the root node though.

I managed to find a dirty workaround though: If I add a dummy that is always rendered in all SSR rendering passes, like this:

import  SortableTree, { getFlatDataFromTree } from '../lib/sortable-tree';
import { observer } from "mobx-react";
import { useQuery } from '../models/reactUtils';
import { Paper } from '@material-ui/core';

const Todos = observer((props) => {
  const {store, loading} = useQuery(store => store.fetchActiveTodoTree());

  return (
    <>
      <TodoItem title="I am here so that my className ID matches :("/> 
      <Paper style={{padding: '20px'}}>
        <SortableTree
          treeData={store.activeTodoTree.toJSON()}
          generateNodeProps={({node, path}) => ({
            title: (
              <TodoItem title={node.title} />
            ),
          })}
        />
      </Paper>
  )
});

Then the issue goes away and everything is rendered perfectly both from the server and upon hydrating in the client. This confirms the theory that the mismatch happens because in the first SSR rendering pass the component is not rendered (as part of the SortableTree).

Environment

"@material-ui/core": "^4.9.10",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.49",
"mobx-react": "^6.1.8",
"mobx-state-tree": "^3.15.0",
"mst-gql": "^0.7.1"
"react": "^16.10.2",
"react-dnd": "7.3.0",
"react-dnd-html5-backend": "7.0.1",
"react-dom": "^16.10.2",
"react-helmet": "^5.2.1",
"react-helmet-async": "^1.0.2",

Browser: Chrome and Firefox, latest versions.


How would I deal with this? I couldn't find out if it's a bug in one of the libraries I'm using (MUI, mst-gql and SortableTree) or if perhaps I missing something.

Let me know if you need any details from my side. Any insights appreciated!

Thanks in advance! 🙇

1
  • 2
    Is it possible for you show a demo of it on something like codesandbox.io ? Commented Apr 25, 2020 at 20:55

1 Answer 1

1

I spent some time trying to extract a minimal example as suggested by @Girish and ended up finding the issue.

It isn't related to material-ui nor mst-gql. It was related to a component being rendered outside a react-router's <Switch>.

I have a <FlashMessage> component that's basically a wrapper around material-ui's <SnackBar>. It used to sit at the bottom of my main App component. Its display is controlled my some observed MST properties. Here's the JSX markup for my App component:

<>
 <CssBaseline />
   <Helmet
     defaultTitle="Foobar"
   />
   <Switch>
     {this.flatRoutes}
   </Switch>
   <FlashMessage />
</>

With the JSX above, the issue reported in my original post still happens. However, If I change it to:

<>
 <CssBaseline />
   <Helmet
     defaultTitle="Foobar"
   />
   <Switch>
     {this.flatRoutes}
      <FlashMessage />
   </Switch>
</>

Then the issue doesn't happen anymore. Notice I moved the <FlashMessage/> component inside 'react-router's <Switch> component.

I still don't know the details of why this was causing the issue. If I ever find out I'll update this post. If anyone else has any insights, please share :)

Sign up to request clarification or add additional context in comments.

1 Comment

Similar issue to yours! thank you for your answer. The issue again was some obscure hidden error within React Router...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.