加载状态+第二次SSR渲染过程导致客户端渲染故障

时间:2020-04-17 17:53:07

标签: reactjs material-ui server-side-rendering

我正在将material-ui与SSR一起使用。我已经根据material-ui文档上的instructions在我的应用上设置了SSR机制。它确实可以工作,但是并非没有渲染问题,到目前为止,这一直很难调试。详细信息如下。

SSR +加载状态(导致所涉及的组件无法在其中一个SSR渲染过程中渲染,有关更多信息,请参见下文)导致在第二个SSR渲染过程中渲染的特定组件的className中的ID不一致。不在第一个位置上(因为它的呈现取决于数据的可用性)。

这会导致从服务器发送的标记为此组件使用不同的CSS类名称,从而导致水化发生时的视觉不一致,如下所示:

SSRed组件:

enter image description here

水合成分:

Hydrated component

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

但是由于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;

还有一个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从后端API加载一些数据,然后传递给SortableTree组件;

从服务器运行时,我使用mst-gql中的getDataFromTree函数来等待数据承诺被解决,并最终将HTML发送回客户端(我已经省略了)此代码来自此处,但可以共享(如果需要的话),就像一个here,只是我的版本使用mst-gql而不是Redux)。请注意,组件树需要呈现两次

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

  2. 然后,一旦兑现了这些承诺,就完成了最后一遍以用可用数据渲染树。

将服务器的标记发送到客户端后,将发生React.hydrate。那时,由于CSS类不存在,因此使用可见的输入呈现了有问题的组件。

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

不过,有意思的是,Foobar节点的子节点即使在水合后也都能按预期呈现,如下所示:

Child tree nodes don't suffer from the SSR rendering glitch when hydration takes place

您会看到这些节点的复选框输入已隐藏,这意味着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>
  )
});

然后问题消失了,无论是从服务器还是在客户端加水时,一切都完美呈现。这证实了发生不匹配的理论,因为在第一次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)中的错误,或者是否丢失了某些东西。

如果您需要我的任何帮助,请告诉我。任何见解表示赞赏!

提前谢谢! ?

1 个答案:

答案 0 :(得分:1)

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

它与material-uimst-gql也无关。它与在react-router的{​​{1}}之外呈现的组件有关。

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

material-ui

使用上述JSX,我的原始帖子中报告的问题仍然存在。但是,如果我将其更改为:

<SnackBar>

然后该问题不再发生。注意,我将<> <CssBaseline /> <Helmet defaultTitle="Foobar" /> <Switch> {this.flatRoutes} </Switch> <FlashMessage /> </> 组件移到了“反应路由器”的<> <CssBaseline /> <Helmet defaultTitle="Foobar" /> <Switch> {this.flatRoutes} <FlashMessage /> </Switch> </> 组件内。

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