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:
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
classNamedid 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:
The first time to trigger any data fetching promises;
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:
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! 🙇


