Состояние загрузки + второй проход рендеринга SSR, вызывающий сбой рендеринга на стороне клиента - PullRequest
2 голосов
/ 17 апреля 2020

Я использую material-ui с SSR. Я настроил механизм SSR в своем приложении в соответствии с инструкциями на material-ui документах. Это работает, но не без проблем рендеринга, которые до сих пор было очень сложно отлаживать. Более подробная информация приведена ниже.

SSR + состояние загрузки (что приводит к тому, что рассматриваемая компания не рендерится на одном из проходов рендеринга SSR, подробнее об этом ниже) приводит к несовместимому идентификатору в className указанного c компонент, который рендерит на втором проходе рендеринга SSR, но не на первом (поскольку его рендеринг обусловлен наличием доступных данных).

Это приводит к тому, что разметка, отправляемая с сервера, имеет другое имя класса 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 к CheckBox input применяется несуществующий класс PrivateSwitchBase-input-411, и он не становится невидимым, как и должно быть, в результате в визуальном сбое при гидратации в т на стороне клиента.

И я получаю следующее предупреждение от 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. Этот компонент загружает некоторые данные из внутреннего интерфейса API с помощью mst-gql и передается компоненту 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 (используется для ввода внутреннего флажка компонента CheckBox в MUI) не соответствует идентификаторов между сервером и клиентом, вызывая визуальный сбой, который я показал на скриншотах выше.

Одна интересная вещь, хотя я s, что дочерние узлы узла Foobar, все рендеринг, как ожидается, даже после гидратации, как вы можете видеть ниже:

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

Вы можете видеть, что входные данные для этих узлов скрыты, Это означает, что класс CSS был применен правильно. Я понятия не имею, почему это происходит только с узлом root.

Мне удалось найти грязный обходной путь, хотя: Если я добавлю манекен, который всегда отображается во всех проходах рендеринга 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).

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 и Firefox, последние версии.


Как бы я справился с этим? Я не мог выяснить, является ли это ошибкой в ​​одной из библиотек, которые я использую (MUI, mst-gql и SortableTree), или, возможно, я что-то упустил.

Дайте мне знать, если вам понадобятся какие-либо подробности с моей стороны. Любые идеи приветствуются!

Спасибо заранее! ?

1 Ответ

0 голосов
/ 27 апреля 2020

Я потратил некоторое время, пытаясь извлечь минимальный пример, предложенный @ Giri sh, и в итоге нашел проблему.

Это не относится ни к material-ui, ни к mst-gql. Это было связано с компонентом, отображаемым вне react-router * <Switch>.

У меня есть <FlashMessage> компонент, который по сути является оберткой вокруг material-ui '* <SnackBar>. Раньше он сидел внизу моего основного компонента приложения. Его отображение контролируется некоторыми наблюдаемыми MST-свойствами. Вот разметка JSX для моего компонента приложения:

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

С JSX выше, проблема, о которой сообщалось в моем исходном посте, все еще возникает. Однако, если я изменю его на:

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

, тогда проблема больше не будет возникать. Обратите внимание, что я переместил компонент <FlashMessage/> внутрь компонента <Switch> реагирующего маршрутизатора.

Я до сих пор не знаю деталей, почему это вызвало проблему. Если я когда-нибудь узнаю, я обновлю этот пост. Если у кого-то есть идеи, пожалуйста, поделитесь:)

...