Ошибка гидратации SSR: начальный интерфейс не совпадает с тем, что было отрендерено на сервере.

Вопрос или проблема

Ошибка гидратации, потому что начальный интерфейс не совпадает с тем, что было отрендерено на сервере.

Файл 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 не совпадает с рендерингом на сервере, это может быть вызвано несколькими причинами.

Причины возникновения ошибки

  1. Несоответствие между состоянием на сервере и клиенте: Если у вас есть данные, которые отличаются на сервере и клиенте во время гидратации, это приводит к ошибкам. Например, использование Math.random() или Date.now() на клиенте может вызвать несоответствие.

  2. Неправильное или несоответствующее состояние компонентов: Если ваши компоненты зависят от состояния, которое инициализируется только на клиенте, это станет причиной разницы в рендеринге UI между сервером и клиентом.

  3. Асинхронные компоненты: Если ваши компоненты загружаются асинхронно с использованием React.lazy(), убедитесь, что сервер возвращает правильный результат при использовании <Suspense>.

  4. Необработанные внешние эффекты: Например, если вы используете библиотеки, которые используют DOM или имеют побочные эффекты, это может нарушить совпадение.

Шаги по устранению проблемы

  1. Проверка состояния данных: Убедитесь, что данные, возвращаемые на сервере, совпадают с данными, которые инициализируются на клиенте. Например, если вы используете библиотеку состояния (Redux, Context API и т. д.), убедитесь, что инициалищация данных происходит одинаково (и на клиенте, и на сервере).

  2. Используйте условную отрисовку: Для компонентов, которые зависят от данных, которые могут измениться между сервером и клиентом, используйте условную отрисовку. Например, проверьте наличие данных перед рендерингом:

    return (
       <>
         {data ? <Component data={data} /> : <LoadingSpinner />}
       </>
    );
  3. Асинхронная загрузка данных: Убедитесь, что все данные загружаются на сервере перед рендерингом. Например, вы можете использовать обещания перед рендерингом компонентов на сервере.

  4. Отладка группировки в "Suspense": Когда вы используете React.lazy(), убедитесь, что ваш серверный рендеринг содержит все отложенные компоненты без ошибочных ссылок. Также проверьте, чтобы пути компонентов были корректными.

  5. Логи и отладка: Включите детализированные логи на сервере, чтобы убедиться, что вы видите то же самое, что и на клиенте. Сравните сгенерированный HTML на сервере и то, что создается на клиенте после гидратации.

Пример отладки вашей конфигурации

В вашем файле AppRouter:

const AppRouter = () => {
  return (
    <Suspense fallback={<Preloader />}>
      <Routes>
        <Route path="/" element={<Layout />}>
          {/* Ваши маршруты */}
        </Route>
      </Routes>
    </Suspense>
  );
};

Убедитесь, что Preloader не рендерит ничего, что зависит от состояния, которое не доступно на сервере. Например, если у Preloader есть анимация или данные на основе таймера, это может вызвать несоответствие.

Проверка конфигурации Webpack

Убедитесь, что ваш Webpack настроен правильно и что файлы JavaScript загружаются правильно. В частности, проверьте, что пути к ресурсам (assets) и динамическим импортеры верны и соответствует тому, что рендерит сервер.

Заключение

В общем, для решения проблемы гидратации необходимо:

  • Проверить соответствие данных между сервером и клиентом.
  • Правильно обрабатывать асинхронные компоненты и использовать Suspense для правильной работы.
  • Убедиться в правильной конфигурации вашей серверной части и использования Webpack.
Оцените материал
Добавить комментарий

Капча загружается...