[英]Loading state + second SSR rendering pass causing a client-side rendering glitch
I'm using material-ui
with SSR.我将
material-ui
与 SSR 一起使用。 I've set up the SSR machinery on my app according to the instructions on the material-ui
docs.我已经根据
material-ui
文档上的说明在我的应用程序上设置了 SSR 机制。 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). SSR + 加载 state(这会导致相关组件无法在其中一个 SSR 渲染通道中渲染,更多内容见下文)导致在第二个 SSR 渲染通道上渲染但不在首先(因为它的渲染取决于有可用的数据)。
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:这会导致从服务器发送的标记具有不同的 CSS class 该组件的名称,从而导致水合发生时的视觉不一致,如下所示:
SSRed component: SSRed 组件:
Hydrated component:水合成分:
The actual class available in the DOM is: DOM 中可用的实际 class 是:
.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. 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:我从 React 收到以下警告:
Warning: Prop
className
did not match.警告:
className
不匹配。 Server: "PrivateSwitchBase-input-411" Client: "PrivateSwitchBase-input-393".服务器:“PrivateSwitchBase-input-411”客户端:“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.我希望
className
匹配并且组件渲染在服务器和客户端中都是相同的。
I have a TodoItem
component:我有一个
TodoItem
组件:
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):还有一个
Todos
组件(简化版):
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.我加载了呈现
Todos
组件的应用程序。 This component loads some data from the backend API using mst-gql and passes over to the SortableTree component;该组件使用mst-gql从后端 API 加载一些数据并传递给SortableTree组件;
When running from the server, I use thegetDataFromTree 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
).从服务器运行时,我使用来自
mst-gql
的getDataFromTree function 等待数据承诺得到解决,最后将 HTML 发送回客户端(我在这里省略了此代码,但可以分享它如果需要。看起来像这里的那个,只是我的版本使用mst-gql
而不是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.在将来自服务器的标记发送到客户端之后,就会发生
React.hydrate
。 That's when the component in question is then rendered with the visible input because of the inexistent CSS class.那时,由于不存在 CSS class,因此使用可见输入呈现有问题的组件。
I'm convinced the problem happens because of point 2
above.我确信问题的发生是因为上面的第
2
点。 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).第一次渲染
Todos
组件时, store.activeTodoTree
数据尚不可用,因此SortableTree
组件不渲染任何内容,因此应该由SortableTree
内联使用的TodoItem
组件作为其树节点(请参阅上面的屏幕截图)不是第一次渲染(但其他一切都是)。 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.我不确切知道
className
ID 后缀生成逻辑在MUI
中是如何工作的,但正因为如此, PrivateSwitchBase-input
class 的后缀(用于 MUI 的 CheckBox 组件的内部复选框输入)在服务器和服务器之间的 ID 不匹配客户端,导致我在上面的屏幕截图中显示的视觉故障。
One interesting thing though, i s that the child nodes of the Foobar
node, all render as expected even after hydration, as you may see below:不过,一件有趣的事情是,
Foobar
节点的子节点即使在水合之后也都按预期呈现,如下所示:
You can see that the checkbox input for these nodes are hidden, which means the CSS class was correctly applied.您可以看到这些节点的复选框输入是隐藏的,这意味着 CSS class 已正确应用。 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:不过,我设法找到了一个肮脏的解决方法:如果我添加一个始终在所有 SSR 渲染通道中渲染的虚拟对象,如下所示:
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).
这证实了不匹配发生的理论,因为在第一个 SSR 渲染过程中,组件没有被渲染(作为 SortableTree 的一部分)。
"@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.浏览器: Chrome 和 Firefox,最新版本。
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.我无法确定这是否是我正在使用的库之一(
MUI
、 mst-gql
和SortableTree
)中的错误,或者我是否遗漏了一些东西。
Let me know if you need any details from my side.如果您需要我这边的任何详细信息,请告诉我。 Any insights appreciated!
任何见解表示赞赏!
Thanks in advance!提前致谢!
I spent some time trying to extract a minimal example as suggested by @Girish and ended up finding the issue.我花了一些时间尝试提取@Girish 建议的最小示例,并最终找到了问题。
It isn't related to material-ui
nor mst-gql
.它与
material-ui
和mst-gql
无关。 It was related to a component being rendered outside a react-router
's <Switch>
.它与在
react-router
的<Switch>
之外呈现的组件有关。
I have a <FlashMessage>
component that's basically a wrapper around material-ui
's <SnackBar>
.我有一个
<FlashMessage>
组件,它基本上是material-ui
的<SnackBar>
的包装器。 It used to sit at the bottom of my main App component.它曾经位于我的主要 App 组件的底部。 Its display is controlled my some observed MST properties.
它的显示由我观察到的一些 MST 属性控制。 Here's the JSX markup for my App component:
这是我的 App 组件的 JSX 标记:
<>
<CssBaseline />
<Helmet
defaultTitle="Foobar"
/>
<Switch>
{this.flatRoutes}
</Switch>
<FlashMessage />
</>
With the JSX above, the issue reported in my original post still happens.使用上面的 JSX,我原来的帖子中报告的问题仍然存在。 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.请注意,我将
<FlashMessage/>
组件移到了 'react-router 的<Switch>
组件中。
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:)
如果其他人有任何见解,请分享:)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.