Проблема
Когда вы пытаетесь выполнить выборку данных на сервере с помощью потокового SSR с Suspense в React 18, вы сталкиваетесь с проблемой, и это несоответствие гидратации. Здесь мы расскажем, как ее решить (решение взято отсюда).
Решение
Вот код серверного приложения:
import express from "express";
import { renderToPipeableStream } from "react-dom/server";
import React from "react";
import AppServer from "../src/components/AppServer";
import path from "path";
import { DataProvider, data } from "../src/providers/data";
import { createServerData } from "../src/api/resource";
import { Writable } from "node:stream";
const app = express();
const port = 3000;
app.get("/", (req, res) => {
const stream = new Writable({
write(chunk, _encoding, cb) {
res.write(chunk, cb);
},
final() {
res.write(
`<script>
window.globalCache={comments:[${data.comments.map((c) => `'${c}'`)}]}
</script>`
);
res.end("</body></html>");
},
});
const { pipe } = renderToPipeableStream(
<DataProvider data={createServerData()}>
<AppServer />
</DataProvider>,
{
bootstrapScripts: ["/main.js"],
onShellReady() {
res.write("<html><body>");
pipe(stream);
},
}
);
});
app.use(express.static(path.join(__dirname, "/../dist")));
app.listen(port, () => {
console.log(`app running on port ${port}`);
});
Ключевой момент находится здесь:
const stream = new Writable({
write(chunk, _encoding, cb) {
res.write(chunk, cb);
},
final() {
res.write(
`<script>
window.globalCache={comments:[${data.comments.map((c) => `'${c}'`)}]}
</script>`
);
res.end("</body></html>");
},
});
Мы пишем скрипт в конце потока, чтобы заполнить переменную globalCache
в браузере данными на сервере.
Именно отсюда берется data
:
import React, { createContext, useContext } from "react";
export let data;
const DataContext = createContext(null);
export function DataProvider({ children, data }) {
return <DataContext.Provider value={data}>{children}</DataContext.Provider>;
}
export function useData() {
const ctx = useContext(DataContext);
if (ctx) {
data = ctx.read();
} else {
data = window.globalCache;
}
return data;
}
На сервере data
считывается из контекста, а в браузере — из переменной globalCache
. Так мы избегаем проблемы несоответствия гидратации.
Давайте посмотрим на функцию createServerData
:
export function createServerData() {
let done = false;
let promise = null;
let value
return {
read: ()=> {
if (done) {
return value
}
if (promise) {
throw promise;
}
promise = new Promise((resolve) => {
setTimeout(() => {
done = true;
promise = null;
value={comments:['a','b','c']}
resolve()
}, 6000);
});
throw promise;
}
};
}
Это обещание, которое разрешается за 6000
мс.
Теперь давайте посмотрим, где мы используем хук useData
, в компоненте Comments
:
import React from "react";
import { useData } from "../providers/data";
export default function Comments() {
const { comments } = useData();
return (
<ul>
{comments && comments.map((comment, i) => <li key={i}>{comment}</li>)}
</ul>
);
}
На сервере он будет читать data
из Context
, в то время как в браузере он будет читать data
из глобальной переменной globalCache
. Это происходит потому, что в браузере контекст не будет определен, потому что в случае браузера мы не оборачиваем компонент App
с DataProvider
:
import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./components/App";
hydrateRoot(document.getElementById("root"), <App />);
Вот как выглядит компонент App
:
import React, { Suspense, lazy } from "react";
const Comments = lazy(() => import("./Comments"));
const App = () => (
<>
<Suspense fallback={<div>loading...</div>}>
<Comments />
</Suspense>
</>
);
export default App;
А вот как выглядит компонент AppServer
, использованный выше (в сервере):
import React from "react";
import App from "./App";
const AppServer = () => (
<div id="root">
<App />
</div>
);
export default AppServer;
На этом мы рассмотрели весь код этого примера о том, как сделать потоковый SSR с Suspense и выборку данных на сервере в React 18, избегая проблемы несоответствия гидратации.