[SSR] Mismatch in className suffix when SSRing a component when the first rendering pass doesn't render the component
See original GitHub issue- The issue is present in the latest release.
- I have searched the issues of this repository and believe that this is not a duplicate.
Current Behavior 😯
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 className for this component, causing a visual inconsistency in when hydration happens:
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 name mismatch, an inexistent class PrivateSwitchBase-input-411
is applied to the CheckBox input, and it’s not made invisible.
And I get the following warning from React:
Warning: Prop `className` did not match. Server: "PrivateSwitchBase-input-411" Client: "PrivateSwitchBase-input-393"
Expected Behavior 🤔
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 page that renders the
Todos
component. This component loads some data using mst-gql and passes over to the SortableTree component; -
When running from the server, I use this function from
mst-gql
to wait for the data promises to be resolved and finally get the HTML to be sent back. Note that the component tree needs to be rendered twice, the first time to trigger any data fetching promises, then these promises are resolved, and finally, the last pass is done to render the tree with the data already 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 visibleinput
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
is not yet available, so the SortableTree
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, 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.
If I add a dummy <TodoItem/>
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 <TodoItem>
component is not rendered (as part of the SortableTree
).
How am I supposed to deal with this? Is this a bug or am I missing something?
Thanks in advance and let me know if you need more information from my side!
Context 🔦
I’m just trying to get my SSRed component to render the same both in the server and the client, without visual glitches upon hydrating because of mismatched class names… 😃
Your 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.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:2
- Comments:11 (3 by maintainers)
Top GitHub Comments
Happens it wasn’t really related to
material-ui
(ormst-gql
). If anyone is curious, you may check my answer here: https://stackoverflow.com/questions/61277705/loading-state-second-ssr-rendering-pass-causing-a-client-side-rendering-glitch.@fullofcaffeine You make sure it’s present or not (NoSsr can help) in both passes.