How to Update Server-Side Rendering in React
Updating Server-Side Rendering (SSR) in React is essential for leveraging modern performance features like HTML streaming and selective hydration. This article explains how to transition legacy React SSR setups—which relied on synchronous rendering—to the modern React 18+ architecture using streaming APIs, update your client-side hydration, and utilize modern frameworks to simplify the process.
Step 1: Replace Legacy Server-Side APIs
Older React SSR configurations relied on renderToString
or renderToNodeStream from the
react-dom/server package. These methods are synchronous and
force the server to wait for the entire HTML page to be generated before
sending anything to the client.
To update your server, replace these legacy APIs with
renderToPipeableStream (for Node.js environments) or
renderToReadableStream (for Web Streams/Edge
environments).
Here is how to implement renderToPipeableStream on an
Express server:
import { renderToPipeableStream } from 'react-dom/server';
app.get('*', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
res.setHeader('content-type', 'text/html');
pipe(res);
},
onShellError(error) {
res.statusCode = 500;
res.send('<!doctype html><p>Loading...</p>');
},
onError(err) {
console.error(err);
}
});
});This updates your SSR setup to stream HTML to the browser incrementally, improving the Time to First Byte (TTFB).
Step 2: Update Client-Side Hydration
Once the server streams the HTML, the client must attach event
listeners to make the page interactive. In React 18+, the legacy
hydrate method has been deprecated.
To update your client-side entry point, import
hydrateRoot from react-dom/client:
// Before (Legacy)
// import { hydrate } from 'react-dom';
// hydrate(<App />, document.getElementById('root'));
// After (Modern)
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);Using hydrateRoot enables Concurrent Features on the
client, allowing React to prioritize user interactions during the
hydration process.
Step 3: Implement Suspense for Selective Hydration
To take full advantage of updated SSR, wrap slow-loading components
in React’s <Suspense> component. This allows the
server to send a placeholder (like a spinner) for slow components, send
the rest of the HTML immediately, and then stream the slow component’s
HTML once its data is ready.
import { Suspense, lazy } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<Header />
<Suspense fallback={<p>Loading content...</p>}>
<HeavyComponent />
</Suspense>
<Footer />
</div>
);
}This prevents slow data fetching or heavy components from blocking the entire page render on the server.
Step 4: Leverage Meta-Frameworks for Seamless Updates
Manually managing custom SSR configurations, bundling, and hydration can become complex. The most reliable way to update SSR in React is by migrating to a modern meta-framework.
- Next.js (App Router): Automatically handles streaming SSR, React Server Components (RSC), and hydration out of the box without manual server configuration.
- Remix: Uses web standards to stream content natively and manages data fetching and SSR synchronization automatically.