加载状态 + 第二个 SSR 渲染通道导致客户端渲染故障

Loading state + second SSR rendering pass causing a client-side rendering glitch

我正在使用 material-ui 和 SSR。我已经根据 material-ui 文档中的 instructions 在我的应用程序上设置了 SSR 机制。它确实有效,但并非没有渲染问题,到目前为止很难调试。更多详情如下。

SSR + 加载状态(这会导致有问题的组件无法在其中一个 SSR 渲染过程中渲染,更多内容见下文)导致渲染的特定组件的 className 中的 ID 不一致在第二次 SSR 渲染过程中而不是在第一次渲染过程中(因为它的渲染以数据可用为条件)。

这会导致从服务器发送的标记对该组件具有不同的 CSS class 名称,从而导致水化发生时的视觉不一致,如下所示:

SSRed组件:

含水成分:

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;
}

但是由于 CSS class 名称不匹配,一个不存在的 class PrivateSwitchBase-input-411 被应用到 CheckBox input,并且它没有被隐藏,本应如此,导致客户端水合作用时出现视觉故障。

我从 React 收到以下警告:

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

我希望 className 匹配并且组件呈现在服务器和客户端中相同。

重现步骤

我有一个 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;

还有一个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>
  )
});

我加载呈现 Todos 组件的应用程序。该组件使用 mst-gql and passes over to the SortableTree 组件从后端 API 加载一些数据;

当来自服务器的 运行 时,我使用 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,只是我的版本使用 mst-gql 而不是 Redux) 。注意组件树需要渲染两次:

  1. 第一次触发任何数据抓取承诺;

  2. 然后一旦这些 promises 得到解决,就完成最后一步以使用可用的数据渲染树。

服务器的标记被发送到客户端后,React.hydrate发生。那时由于 CSS class.

不存在,因此使用可见输入呈现有问题的组件

我确信问题的发生是因为上面的 2 点。 第一次渲染 Todos 组件时, store.activeTodoTree 数据尚不可用,因此 SortableTree 组件不呈现任何内容,因此 TodoItem 组件应该被 SortableTree 内联用作其树节点(请参阅上面的屏幕截图)不是第一次渲染(但其他一切都是)。我不确切知道 className ID 后缀生成逻辑在 MUI 中是如何工作的,但正因为如此,PrivateSwitchBase-input class 的后缀(用于 MUI 的 CheckBox 组件的内部复选框输入)在服务器和客户端之间的 ID 不匹配,导致我在上面的屏幕截图中显示的视觉故障。

不过有一件有趣的事情,我Foobar 节点的子节点,即使在水合作用后也会按预期呈现,如下所示:

您可以看到这些节点的复选框输入是隐藏的,这意味着 CSS class 已正确应用。我不知道为什么这只发生在根节点上。

虽然我设法找到了一个肮脏的解决方法:如果我添加一个始终在所有 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>
  )
});

然后问题就消失了,所有内容都在服务器和客户端中完美呈现。这证实了不匹配发生的理论,因为在第一个 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",

浏览器: Chrome 和 Firefox,最新版本。


我该如何处理?我无法确定它是否是我正在使用的库之一(MUImst-gqlSortableTree)中的错误,或者我是否遗漏了什么。

如果您需要我这边的任何详细信息,请告诉我。任何见解表示赞赏!

提前致谢!

我花了一些时间尝试提取@Girish 建议的最小示例,并最终找到了问题所在。

它与 material-uimst-gql 无关。它与在 react-router<Switch>.

之外呈现的组件有关

我有一个 <FlashMessage> 组件,它基本上是 material-ui<SnackBar> 的包装器。它曾经位于我的主要 App 组件的底部。它的显示由我观察到的一些 MST 属性控制。这是我的 App 组件的 JSX 标记:

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

使用上面的 JSX,我原来 post 中报告的问题仍然会发生。但是,如果我将其更改为:

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

那么问题就没有了。请注意,我将 <FlashMessage/> 组件移到了“react-router”的 <Switch> 组件中。

我仍然不知道导致此问题的详细原因。如果我发现了,我会更新这个 post。如果其他人有任何见解,请分享:)