Я использую material-ui
с SSR. Я настроил механизм SSR в своем приложении в соответствии с инструкциями на material-ui
документах. Это работает, но не без проблем рендеринга, которые до сих пор было очень сложно отлаживать. Более подробная информация приведена ниже.
SSR + состояние загрузки (что приводит к тому, что рассматриваемая компания не рендерится на одном из проходов рендеринга SSR, подробнее об этом ниже) приводит к несовместимому идентификатору в className указанного c компонент, который рендерит на втором проходе рендеринга 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;
}
Но из-за несоответствия имен классов 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
). Обратите внимание, что дерево компонентов должно быть обработано дважды :
Первый раз, чтобы инициировать любые обещания выборки данных;
Затем, после выполнения этих обещаний, выполняется последний проход для визуализации дерева с данными, которые стали доступны.
После разметки с сервера отправляется клиенту, затем React.hydrate
. Именно тогда рассматриваемый компонент отображается с видимым вводом из-за несуществующего класса CSS.
Я уверен, что проблема возникает из-за пункта 2
выше. При первом рендеринге компонента Todos
данные store.activeTodoTree
еще не доступны, поэтому компонент SortableTree
ничего не рендерит, поэтому компонент TodoItem
, который должен использоваться SortableTree
как встроенный его узлы дерева (см. скриншоты выше) не отображаются в первый раз (но все остальное есть). Я не знаю точно, как логики генерации суффикса className
ID работают в MUI
, но из-за этого суффикс для класса PrivateSwitchBase-input
(используется для ввода внутреннего флажка компонента CheckBox в MUI) не соответствует идентификаторов между сервером и клиентом, вызывая визуальный сбой, который я показал на скриншотах выше.
Одна интересная вещь, хотя я s, что дочерние узлы узла Foobar
, все рендеринг, как ожидается, даже после гидратации, как вы можете видеть ниже:
Вы можете видеть, что входные данные для этих узлов скрыты, Это означает, что класс 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
), или, возможно, я что-то упустил.
Дайте мне знать, если вам понадобятся какие-либо подробности с моей стороны. Любые идеи приветствуются!
Спасибо заранее! ?