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

Ful*_*ine 5 reactjs material-ui server-side-rendering

我正在material-ui与SSR一起使用。我已经根据文档上的说明material-ui在我的应用程序上设置了 SSR 机制。它确实有效,但并非没有渲染问题,迄今为止很难调试。更多详细信息如下。

SSR + 加载状态(这会导致相关组件无法在其中一个 SSR 渲染通道中渲染,更多内容见下文)导致在第二个 SSR 渲染通道上渲染的特定组件的 className 中 ID 不一致,但不会在第二个 SSR 渲染通道上渲染首先(因为它的渲染以数据可用为条件)。

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

SSRed 成分:

在此输入图像描述

水合成分:

水合成分

DOM 中实际可用的类是:

.PrivateSwitchBase-input-393 {
  top: 0;
  left: 0;
  width: 100%;
  cursor: inherit;
  height: 100%;
  margin: 0;
  opacity: 0;
  padding: 0;
  z-index: 1;
  position: absolute;
}
Run Code Online (Sandbox Code Playgroud)

但由于 CSS 类名不匹配,一个不存在的类PrivateSwitchBase-input-411被应用于 CheckBox input,并且它没有像应该的那样变得不可见,导致客户端水合时出现视觉故障。

我从 React 收到以下警告:

警告:道具className不匹配。服务器:“PrivateSwitchBase-input-411”客户端:“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;
Run Code Online (Sandbox Code Playgroud)

和一个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>
  )
});
Run Code Online (Sandbox Code Playgroud)

我加载呈现该组件的应用程序Todos该组件使用mst-gql从后端 API 加载一些数据,并将其传递给SortableTree组件;

从服务器运行时,我使用getDataFromTree函数来mst-gql等待数据承诺得到解析,并最终将 HTML 发送回客户端(我在此处省略了此代码,但如果需要,可以共享它。它看起来像这里的那个,只是我的版本使用mst-gql而不是Redux)。请注意,组件树需要渲染两次

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

  2. 然后,一旦解决了这些承诺,就会完成最后一次传递,以使用可用的数据来渲染树。

将标记从服务器发送到客户端后,就会React.hydrate发生。此时,由于 CSS 类不存在,相关组件将使用可见输入进行渲染。

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

不过,一件有趣的事情是,即使在水合之后,该节点的子节点Foobar也会按预期渲染,如下所示:

当发生水合作用时,子树节点不会受到 SSR 渲染故障的影响

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

不过,我设法找到了一个肮脏的解决方法:如果我添加一个始终在所有 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>
  )
});
Run Code Online (Sandbox Code Playgroud)

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

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


我该如何处理这个问题?我无法确定这是否是我正在使用的库之一(MUImst-gqlSortableTree)中的错误,或者我是否遗漏了某些内容。

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

提前致谢!

Ful*_*ine 1

我花了一些时间尝试按照@Girish 的建议提取一个最小的示例,最终发现了问题。

它与material-ui或 无关mst-gqlreact-router它与在a之外渲染的组件有关<Switch>

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

<>
 <CssBaseline />
   <Helmet
     defaultTitle="Foobar"
   />
   <Switch>
     {this.flatRoutes}
   </Switch>
   <FlashMessage />
</>
Run Code Online (Sandbox Code Playgroud)

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

<>
 <CssBaseline />
   <Helmet
     defaultTitle="Foobar"
   />
   <Switch>
     {this.flatRoutes}
      <FlashMessage />
   </Switch>
</>
Run Code Online (Sandbox Code Playgroud)

那么这个问题就不会再发生了。请注意,我将该<FlashMessage/>组件移至“react-router”<Switch>组件内。

我仍然不知道为什么会导致这个问题的详细信息。如果我发现我会更新这篇文章。如果其他人有任何见解,请分享:)