Вопрос или проблема
Ошибка гидратации, потому что начальный интерфейс не совпадает с тем, что было отрендерено на сервере.
Файл main.tsx – Попытка реализовать SSR, но возникает проблема несоответствия интерфейса.
import ReactDOM, { hydrateRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { BrowserRouter } from "react-router-dom";
import AppRouter from "./Navigator/AppRouter.tsx";
import { GlobalProvider } from "./Context/GlobalContextProvider.tsx";
import { ToastContainer } from "react-toast";
import "../index.css";
import "react-datepicker/dist/react-datepicker.css";
import "react-date-picker/dist/DatePicker.css";
import "react-calendar/dist/Calendar.css";
import { Provider } from "react-redux";
import { AccessTokenProvider } from "@Context/AccessTokenContextProvider.tsx";
import store from "./store/store.ts";
import React from "react";
// Создаем новый экземпляр QueryClient
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 10000,
},
},
});
// Получаем корневой элемент и уверяемся, что он не равен null
const rootElement = document.getElementById('root');
if (rootElement) {
hydrateRoot(
rootElement, // Передаем корневой элемент здесь
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<GlobalProvider>
<AccessTokenProvider>
<AppRouter />
</AccessTokenProvider>
<ToastContainer delay={6000} position="top-center" />
</GlobalProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</Provider>
</BrowserRouter>
</React.StrictMode>
);
} else {
console.error("Корневой элемент не найден. Невозможно гидратировать.");
}
Файл AppRouter – Файл маршрутизатора приложения содержит все маршруты.
import { Navigate, Outlet, Route, Routes } from "react-router-dom";
import { Suspense, lazy } from "react";
import { AppRoute } from "./AppRoute";
import Preloader from "@Components/Loader";
import ForgotPassword from "@Container/Auth/ForgotPassword/ForgotPassword";
import RejectedJob from "@Container/Dashboard/Container/RejectedJob/RejectedJob";
import AccptedJob from "@Container/Dashboard/Container/AccptedJob/AcceptedJob";
import Error from "@Container/Error/Error";
import JobDetailsUpdate from "@Container/JobDetail/JobDetailsUpdate";
import ShareProfile from "@Container/Dashboard/Container/Profile/ShareProfile";
const BlogDetails = lazy(() => import("@Container/Blog/BlogDetails"));
const ChooseTemplate = lazy(
() => import("@Container/Resume Builder/Choose Template/ChooseTemplate")
);
const ResumeDesignsOne = lazy(
() =>
import(
"@Container/Resume Builder/Resume Designs/Resume-one/ResumeDesignsOne"
)
);
const Info = lazy(() => import("@Container/Resume Builder/Info/Info"));
const ResumeIndex = lazy(() => import("@Container/Resume Builder/ResumeIndex"));
const ResumeMain = lazy(
() => import("@Container/Resume Builder/Resume Main/ResumeMain")
);
const Applied = lazy(
() => import("@Container/Dashboard/Container/Applied/Applied")
);
const Profile = lazy(
() => import("@Container/Dashboard/Container/Profile/Profile")
);
const DashboardLayout = lazy(
() => import("@Container/Dashboard/DashboardLayout")
);
const UserDashboard = lazy(
() => import("@Container/Dashboard/Container/UserDashboard/UserDashboard")
);
const Resume = lazy(
() => import("@Container/Dashboard/Container/Resume/Resume")
);
const SavedJob = lazy(
() => import("@Container/Dashboard/Container/SavedJob/SavedJob")
);
const AccountSetting = lazy(
() => import("@Container/Dashboard/Container/AccountSetting/AccountSetting")
);
const Home = lazy(() => import("../Container/Home/Home"));
// const JobListing = lazy(() => import("@Container/JobListing/JobListing"));
const JobListing = lazy(() => import("@Container/JobListing/FilterJob"));
const Login = lazy(() => import("@Container/Auth/Login/Login"));
const SignUp = lazy(() => import("@Container/Auth/SignUp/SignUp"));
const Layout = lazy(() => import("@Container/Layout/Layout"));
const Membership = lazy(
() => import("@Container/Dashboard/Container/Membership/Membership")
);
const ApplyJob = lazy(() => import("@Container/ApplyJob/ApplyJob"));
const PublicRoute = lazy(() => import("./PublicRoute/PublicRoute"));
const PrivateRoute = lazy(() => import("./PrivateRoute/PrivateRoute"));
const Thankyou = lazy(() => import("@Container/ThankYou/ThankYou"));
// const Error = lazy(() => import("@Container/Error/Error"));
const ContactUs = lazy(() => import("@Container/contactUs/ContactUs"));
const Subjects = lazy(() => import("@Container/Subjects/Subjects"));
const Aboutus = lazy(() => import("@Container/Aboutus/Aboutus"));
const Faq = lazy(() => import("@Container/Faq/Faq"));
const Termsofuse = lazy(() => import("@Container/Termsofuse"));
const Blog = lazy(() => import("@Container/Blog/Blog"));
const Privacypolicy = lazy(
() => import("@Container/privacypolicy/Privacypolicy")
);
const RefundPolicy = lazy(() => import("@Container/RefundPolicy/RefundPolicy"));
const AppRouter = () => {
return (
<Suspense fallback={<Preloader />}>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="*" element={<Error />} />
{/* <Route path="/signin-linkedin" component={LinkedInCallback} /> */}
<Route path={`${AppRoute.Find_Jobs}`} element={<JobListing />} />
<Route path={AppRoute.Home} element={<Home />} />
<Route path={AppRoute.Faqs} element={<Faq />} />
<Route path={AppRoute.About_Us} element={<Aboutus />} />
<Route path={AppRoute.Terms_of_use} element={<Termsofuse />} />
<Route path={AppRoute.Privacy_Policy} element={<Privacypolicy />} />
<Route path={AppRoute.Contact_Us} element={<ContactUs />} />
<Route path={AppRoute.Refund_Policy} element={<RefundPolicy />} />
<Route path={AppRoute.Blog} element={<Blog />} />
{/* <Route path={AppRoute.Careers} element={<Careers />} /> */}
<Route
path={`${AppRoute.Blog}/:title/:id`}
element={<BlogDetails />}
/>
<Route path={`:category/:sub_id`} element={<Subjects />} />
<Route
path={`${AppRoute.Find_Jobs}/:title/:id/:state/:city/:range/:experience`}
element={<JobDetailsUpdate />}
/>
<Route
path={`${AppRoute.Find_Jobs}/:category/:subjects`}
element={<JobListing />}
/>
<Route
path="/"
element={
<PrivateRoute>
<Outlet />
</PrivateRoute>
}
>
<Route path={AppRoute.ResumeIndex} element={<ResumeIndex />}>
{/* Перенаправление на Info при доступе к ResumeIndex */}
<Route
index
element={<Navigate to={AppRoute.Info} replace />}
/>
<Route path={AppRoute.Info} element={<Info />} />
<Route
path={AppRoute.ChooseTemplate}
element={<ChooseTemplate />}
/>
<Route path={AppRoute.ResumeMain} element={<ResumeMain />} />
<Route
path={AppRoute.ResumeDesignOne}
element={<ResumeDesignsOne />}
/>
</Route>
{/* Другие частные маршруты */}
</Route>
<Route
path="/"
element={
<PrivateRoute>
<Outlet />
</PrivateRoute>
}
>
<Route path={AppRoute.ShareProfile} element={<ShareProfile />} />
<Route path={AppRoute.Thank_You} element={<Thankyou />} />
<Route path={AppRoute.Dashboard} element={<DashboardLayout />}>
<Route path="*" element={<Error />} />
<Route path={AppRoute.Profile} element={<Profile />} />
<Route
path={`${AppRoute.User_Dashboard}`}
element={<UserDashboard />}
/>
<Route path={`${AppRoute.Resume}`} element={<Resume />} />
<Route path={`${AppRoute.Saved_Job}`} element={<SavedJob />} />
<Route path={`${AppRoute.Applied_Job}`} element={<Applied />} />
<Route
path={`${AppRoute.Accepted_Job}`}
element={<AccptedJob />}
/>
<Route
path={`${AppRoute.Rejected_Job}`}
element={<RejectedJob />}
/>
<Route
path={`${AppRoute.Account_Setting}`}
element={<AccountSetting />}
/>
<Route
path={`${AppRoute.Membership}`}
element={<Membership />}
/>
</Route>
<Route path={`${AppRoute.Apply_job}`} element={<ApplyJob />} />
</Route>
<Route
path="/"
element={
<PublicRoute>
<Outlet />
</PublicRoute>
}
>
<Route path={AppRoute.ShareProfile} element={<ShareProfile />} />
<Route path={AppRoute.Login} element={<Login />} />
<Route path={AppRoute.SignUp} element={<SignUp />} />
<Route
path={AppRoute.Forgot_password}
element={<ForgotPassword />}
/>
</Route>
</Route>
</Routes>
</Suspense>
);
};
export default AppRouter;
Файл конфигурации Webpack –
import path from 'path';
import webpack from 'webpack';
import htmlWebpackPlugin from 'html-webpack-plugin';
/**
* Загружаем файлы JS, JSX, TS и TSX через Babel
*/
const babelLoader = {
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
['@babel/preset-react', { runtime: 'automatic' }]
],
},
},
};
// Общая конфигурация разрешений
const resolve = {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'@': path.resolve(__dirname, 'src'), // Устанавливаем псевдоним для @
'@Context': path.resolve(__dirname, 'src/Context'),
'@Assets': path.resolve(__dirname, 'src/assets'),
'@Container': path.resolve(__dirname, 'src/Container'),
'@Hooks': path.resolve(__dirname, 'src/Hooks'),
'@Navigator': path.resolve(__dirname, 'src/Navigator'),
'@Components': path.resolve(__dirname, 'src/Components'),
'@Repo': path.resolve(__dirname, 'src/Repo'),
'@Utils': path.resolve(__dirname, 'src/Utils'),
'@Const': path.resolve(__dirname, 'src/Const'),
},
};
const serverConfig = {
target: 'node',
mode: 'development',
entry: './server/server.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'server.cjs',
},
module: {
rules: [
babelLoader,
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
use: [
{
loader: 'file-loader',
options: {
name: '[path][name].[ext]',
},
},
],
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader', // Добавляет поддержку Tailwind CSS
options: {
postcssOptions: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
},
},
},
],
},
{
test: /\.scss$/,
use: [
'style-loader', // Внедряет стили в DOM
'css-loader', // Разрешает импорты CSS
'sass-loader', // Компилирует SCSS в CSS
],
},
],
},
plugins: [
new webpack.EnvironmentPlugin({
PORT: 3001,
}),
],
resolve: resolve,
};
const clientConfig = {
target: 'web',
mode: 'development',
entry: './src/main.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/static',
filename: 'client.js',
},
module: {
rules: [
babelLoader,
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader', // Добавляет поддержку Tailwind CSS
options: {
postcssOptions: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
},
},
},
],
},
{
test: /\.scss$/,
use: [
'style-loader', // Внедряет стили в DOM
'css-loader', // Разрешает импорты CSS
'sass-loader', // Компилирует SCSS в CSS
],
},
// Правило для изображений
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset/resource',
generator: {
filename: '[path][name].[ext]',
},
},
],
},
plugins: [
new htmlWebpackPlugin({
template: path.resolve(__dirname, './index.html'),
}),
],
resolve: resolve,
};
export default [serverConfig, clientConfig];
Файл Server.js
import React from "react";
import express from "express";
import { StaticRouter } from "react-router-dom/server";
import ReactDOMServer from "react-dom/server";
import AppRouter from "../src/Navigator/AppRouter";
import fs from "fs";
const app = express();
const PORT = 3001;
/**
* @param {string} location
* @return {string}
*/
const createReactApp = async (location) => {
const reactApp = ReactDOMServer.renderToString(
<StaticRouter location={location}>
<AppRouter />
</StaticRouter>
);
const html = await fs.promises.readFile(`${__dirname}/index.html`, "utf-8");
const reactHtml = html.replace(
'<div id="root"></div>',
`<div id="root">${reactApp}</div>`
);
return reactHtml;
};
app.use("/static", express.static(__dirname));
app.get("*", async (req, res) => {
const reactApp = await createReactApp(req.url);
res.status(200).send(reactApp);
});
app.listen(PORT, () => {
console.log(`Сервер запущен на порту ${PORT}`);
});
Пытается реализовать SSR на React, но возникает эта ошибка.
2react-dom.development.js:12507 Uncaught Error: Ошибка гидратации, потому что начальный интерфейс не совпадает с тем, что было отрендерено на сервере. at throwOnHydrationMismatch (react-dom.development.js:12507:9) at tryToClaimNextHydratableInstance (react-dom.development.js:12520:7) at updateHostComponent (react-dom.development.js:19897:5) at beginWork (react-dom.development.js:21613:14) at beginWork$1 (react-dom.development.js:27421:14) at performUnitOfWork (react-dom.development.js:26552:12) at workLoopSync (react-dom.development.js:26461:5) at renderRootSync (react-dom.development.js:26429:7) at performConcurrentWorkOnRoot (react-dom.development.js:25733:74) at workLoop (scheduler.development.js:266:34) throwOnHydrationMismatch @ react-dom.development.js:12507 tryToClaimNextHydratableInstance @ react-dom.development.js:12520 updateHostComponent @ react-dom.development.js:19897 beginWork @ react-dom.development.js:21613 beginWork$1 @ react-dom.development.js:27421 performUnitOfWork @ react-dom.development.js:26552 workLoopSync @ react-dom.development.js:26461 renderRootSync @ react-dom.development.js:26429 performConcurrentWorkOnRoot @ react-dom.development.js:25733 workLoop @ scheduler.development.js:266 flushWork @ scheduler.development.js:239 performWorkUntilDeadline @ scheduler.development.js:533 Показать еще 12 кадров Показать меньше react-dom.development.js:19844 Uncaught Error: Произошла ошибка при гидратации. Поскольку ошибка произошла вне границы Suspense, весь корень перейдет на клиентский рендеринг. at updateHostRoot (react-dom.development.js:19844:57) at beginWork (react-dom.development.js:21610:14) at beginWork$1 (react-dom.development.js:27421:14) at performUnitOfWork (react-dom.development.js:26552:12) at workLoopSync (react-dom.development.js:26461:5) at renderRootSync (react-dom.development.js:26429:7) at recoverFromConcurrentError (react-dom.development.js:25845:20) at performConcurrentWorkOnRoot (react-dom.development.js:25745:22) at workLoop (scheduler.development.js:266:34) at flushWork (scheduler.development.js:239:14) updateHostRoot @ react-dom.development.js:19844 beginWork @ react-dom.development.js:21610 beginWork$1 @ react-dom.development.js:27421 performUnitOfWork @ react-dom.development.js:26552 workLoopSync @ react-dom.development.js:26461 renderRootSync @ react-dom.development.js:26429 recoverFromConcurrentError @ react-dom.development.js:25845 performConcurrentWorkOnRoot @ react-dom.development.js:25745 workLoop @ scheduler.development.js:266 flushWork @ scheduler.development.js:239 performWorkUntilDeadline @ scheduler.development.js:533 Показать еще 11 кадров Показать меньше staticvendors-node_modules_prop-types_index_js.client.js:1 Uncaught SyntaxError: Neожиданный токен '<' (в staticvendors-node_modules_prop-types_index_js.client.js:1:1) staticvendors-node_modules_react-tabs_esm_index_js.client.js:1 Uncaught SyntaxError: Neожиданный токен '<' (в staticvendors-node_modules_react-tabs_esm_index_js.client.js:1:1) staticsrc_Container_Layout_Layout_tsx.client.js:1 Uncaught SyntaxError: Neожиданный токен '<' (в staticsrc_Container_Layout_Layout_tsx.client.js:1:1) 2react.development.js:1408 Uncaught ChunkLoadError: Ошибка загрузки фрагмента vendors-node_modules_prop-types_index_js не удалась. (пропало: http://localhost:3001/staticvendors-node_modules_prop-types_index_js.client.js) at __webpack_require__.f.j (client.js:3114) at client.js:2983:40 at Array.reduce (<anonymous>) at __webpack_require__.e (client.js:2982:67) at eval (AppRouter.tsx:82:58) at lazyInitializer (react.development.js:1358:20) at mountLazyComponent (react-dom.development.js:19939:19) at beginWork (react-dom.development.js:21588:16) at beginWork$1 (react-dom.development.js:27421:14) at performUnitOfWork (react-dom.development.js:26552:12) __webpack_require__.f.j @ client.js:3114 (анонимно) @ client.js:2983 __webpack_require__.e @ client.js:2982 eval @ AppRouter.tsx:82 lazyInitializer @ react.development.js:1358 mountLazyComponent @ react-dom.development.js:19939 beginWork @ react-dom.development.js:21588 beginWork$1 @ react-dom.development.js:27421:14) performUnitOfWork @ react-dom.development.js:26552:12) __webpack_require__.f.j @ client.js:3114 (анонимно) @ client.js:2983 __webpack_require__.e @ client.js:2982 eval @ AppRouter.tsx:82 lazyInitializer @ react.development.js:1358 mountLazyComponent @ react-dom.development.js:19939 beginWork @ react-dom.development.js:21588 beginWork$1 @ react-dom.development.js:27421:14) performUnitOfWork @ react-dom.development.js:26552:12) performConcurrentWorkOnRoot @ react-dom.development.js:25745) Показать еще 15 кадров Показать меньше react-dom.development.js:26918 Uncaught ChunkLoadError: Ошибка загрузки фрагмента vendors-node_modules_prop-types_index_js не удалась. (пропало: http://localhost:3001/staticvendors-node_modules_prop-types_index_js.client.js) at __webpack_require__.f.j (client.js:3114:29) at client.js:2983:40) at Array.reduce (<анонимно>) at __webpack_require__.e (client.js:2982:67) at eval (AppRouter.tsx:82:58) at lazyInitializer (react.development.js:1358:20) at mountLazyComponent (react-dom.development.js:19939:19) at beginWork (react-dom.development.js:21588:16) at beginWork$1 @ react-dom.development.js:27421:14) performUnitOfWork @ react-dom.development.js:26552:12) performConcurrentWorkOnRoot @ react-dom.development.js:25745) Показать еще 12 кадров Показать меньше staticvendors-node_modules_tanstack_query-devtools_build_Devtools_TCDAW7I4_js.client.js:1 Uncaught SyntaxError: Neожиданный токен '<' (в staticvendors-node_modules_tanstack_query-devtools_build_Devtools_TCDAW7I4_js.client.js:1:1)
Ответ или решение
Проблема: Ошибка гидратации из-за несоответствия UI, созданного на сервере
Когда вы пытаетесь реализовать серверный рендеринг (SSR) в React и получаете ошибку, связанную с тем, что начальный UI не совпадает с рендерингом на сервере, это может быть вызвано несколькими причинами.
Причины возникновения ошибки
-
Несоответствие между состоянием на сервере и клиенте: Если у вас есть данные, которые отличаются на сервере и клиенте во время гидратации, это приводит к ошибкам. Например, использование
Math.random()
илиDate.now()
на клиенте может вызвать несоответствие. -
Неправильное или несоответствующее состояние компонентов: Если ваши компоненты зависят от состояния, которое инициализируется только на клиенте, это станет причиной разницы в рендеринге UI между сервером и клиентом.
-
Асинхронные компоненты: Если ваши компоненты загружаются асинхронно с использованием
React.lazy()
, убедитесь, что сервер возвращает правильный результат при использовании<Suspense>
. -
Необработанные внешние эффекты: Например, если вы используете библиотеки, которые используют DOM или имеют побочные эффекты, это может нарушить совпадение.
Шаги по устранению проблемы
-
Проверка состояния данных: Убедитесь, что данные, возвращаемые на сервере, совпадают с данными, которые инициализируются на клиенте. Например, если вы используете библиотеку состояния (Redux, Context API и т. д.), убедитесь, что инициалищация данных происходит одинаково (и на клиенте, и на сервере).
-
Используйте условную отрисовку: Для компонентов, которые зависят от данных, которые могут измениться между сервером и клиентом, используйте условную отрисовку. Например, проверьте наличие данных перед рендерингом:
return ( <> {data ? <Component data={data} /> : <LoadingSpinner />} </> );
-
Асинхронная загрузка данных: Убедитесь, что все данные загружаются на сервере перед рендерингом. Например, вы можете использовать обещания перед рендерингом компонентов на сервере.
-
Отладка группировки в "Suspense": Когда вы используете
React.lazy()
, убедитесь, что ваш серверный рендеринг содержит все отложенные компоненты без ошибочных ссылок. Также проверьте, чтобы пути компонентов были корректными. -
Логи и отладка: Включите детализированные логи на сервере, чтобы убедиться, что вы видите то же самое, что и на клиенте. Сравните сгенерированный HTML на сервере и то, что создается на клиенте после гидратации.
Пример отладки вашей конфигурации
В вашем файле AppRouter
:
const AppRouter = () => {
return (
<Suspense fallback={<Preloader />}>
<Routes>
<Route path="/" element={<Layout />}>
{/* Ваши маршруты */}
</Route>
</Routes>
</Suspense>
);
};
Убедитесь, что Preloader
не рендерит ничего, что зависит от состояния, которое не доступно на сервере. Например, если у Preloader
есть анимация или данные на основе таймера, это может вызвать несоответствие.
Проверка конфигурации Webpack
Убедитесь, что ваш Webpack настроен правильно и что файлы JavaScript загружаются правильно. В частности, проверьте, что пути к ресурсам (assets) и динамическим импортеры верны и соответствует тому, что рендерит сервер.
Заключение
В общем, для решения проблемы гидратации необходимо:
- Проверить соответствие данных между сервером и клиентом.
- Правильно обрабатывать асинхронные компоненты и использовать
Suspense
для правильной работы. - Убедиться в правильной конфигурации вашей серверной части и использования Webpack.