You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React服务端渲染最佳方式及多技术栈下渲染与分割方案问询

Implementing SSR with React/Redux Saga/React Router/Node + Code/Style Splitting

Great question—let’s walk through a practical, production-ready approach to get SSR working smoothly with your tech stack, including code and style splitting. I’ve helped teams troubleshoot this exact setup before, so I’ll focus on the pain points you’re likely hitting (like data prefetching sync, saga integration, and splitting that doesn’t break SSR).

1. Project Structure Foundation

First, organize your code to separate server-side and client-side entry points, plus shared components—this keeps things clean and avoids duplication:

src/
├── client/          # Client-side hydration logic
│   └── index.js
├── server/          # Node.js SSR server setup
│   └── index.js
├── shared/          # Reusable code (components, routes, store, sagas)
│   ├── components/
│   ├── routes/
│   ├── store/
│   └── sagas/
└── public/          # Static assets (fonts, images)

2. Configure React Router for SSR

You need a shared route config that both server and client can use. Add a loadData method to routes that require prefetching data via Redux Saga, and use SSR-friendly code splitting tools.

Shared Route File (src/shared/routes.js)

import loadable from '@loadable/component';
import Home from './components/Home';

// Code-split the About component (SSR-safe)
const About = loadable(() => import('./components/About'), {
  fallback: <div>Loading...</div>
});

export const routes = [
  {
    path: '/',
    component: Home,
    exact: true
  },
  {
    path: '/about',
    component: About,
    // Define data-fetching logic for this route
    loadData: (store) => store.dispatch({ type: 'FETCH_ABOUT_CONTENT' })
  }
];

Client-Side Router (src/client/index.js)

import React from 'react';
import { hydrate } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { LoadableCapture } from '@loadable/component';
import App from '../shared/App';
import createStore from '../shared/store';

// Initialize store with server-provided initial state
const store = createStore(window.__INITIAL_STATE__);

hydrate(
  <LoadableCapture>
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  </LoadableCapture>,
  document.getElementById('root')
);

3. Integrate Redux Saga with SSR

The biggest challenge here is syncing saga data fetching on the server before rendering. You’ll need to run sagas, wait for them to complete, then pass the final state to the client.

Store Configuration (src/shared/store/index.js)

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from '../sagas';

export default (initialState = {}) => {
  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(
    rootReducer,
    initialState,
    applyMiddleware(sagaMiddleware)
  );

  // Expose the saga task to wait for completion on the server
  store.sagaTask = sagaMiddleware.run(rootSaga);
  return store;
};

Server-Side Saga Handling

In your Node server, after dispatching loadData actions, wait for all sagas to finish before rendering:

// Inside your server render route handler
const store = createStore();

// Match the current route and trigger data fetching
const matchingRoutes = matchRoutes(routes, req.path);
const loadDataPromises = matchingRoutes
  .map(({ route }) => route.loadData?.(store))
  .filter(Boolean);

// Wait for all data-fetching actions to dispatch
await Promise.all(loadDataPromises);
// Wait for the saga task to complete all background work
await store.sagaTask.toPromise();

4. Code Splitting (SSR-Friendly)

React’s built-in React.lazy doesn’t work with SSR, so use @loadable/component—it’s purpose-built for SSR and handles chunk loading seamlessly.

Install Dependencies

npm install @loadable/component @loadable/server

Server-Side Chunk Collection

Use @loadable/server to collect split chunks and inject them into your HTML template:

import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import { Provider } from 'react-redux';
import { ChunkExtractor } from '@loadable/server';
import App from '../shared/App';
import routes from '../shared/routes';

// Initialize chunk extractor with webpack stats file
const extractor = new ChunkExtractor({
  statsFile: path.resolve(__dirname, '../../dist/loadable-stats.json'),
});

// Render app and collect chunks
const appHtml = renderToString(
  extractor.collectChunks(
    <Provider store={store}>
      <StaticRouter location={req.path}>
        <App />
      </StaticRouter>
    </Provider>
  )
);

// Inject chunks into HTML
const html = `
  <!DOCTYPE html>
  <html>
    <head>
      ${extractor.getLinkTags()}
      ${extractor.getStyleTags()}
    </head>
    <body>
      <div id="root">${appHtml}</div>
      <script>window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())}</script>
      ${extractor.getScriptTags()}
    </body>
  </html>
`;

5. Style Splitting

For style splitting that works with SSR, choose one of these reliable options:

Option A: CSS Modules + MiniCssExtractPlugin

Configure webpack to extract CSS into separate chunks for both client and server:

Webpack Client Config

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.module\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          { loader: 'css-loader', options: { modules: true } }
        ]
      }
    ]
  },
  plugins: [new MiniCssExtractPlugin()]
};

Server-Side CSS Handling

On the server, use css-loader/locals to avoid generating actual CSS files:

module.exports = {
  module: {
    rules: [
      {
        test: /\.module\.css$/,
        use: [{ loader: 'css-loader', options: { modules: true, onlyLocals: true } }]
      }
    ]
  }
};

Option B: Styled Components

If you use styled-components, add the babel-plugin-styled-components and collect styles server-side:

import { ServerStyleSheet } from 'styled-components';

// In your server render function
const sheet = new ServerStyleSheet();
try {
  const appHtml = renderToString(
    sheet.collectStyles(
      <Provider store={store}>
        <StaticRouter location={req.path}>
          <App />
        </StaticRouter>
      </Provider>
    )
  );
  const styleTags = sheet.getStyleTags();
  // Inject styleTags into the HTML head
} finally {
  sheet.seal();
}

6. Final Express Server Setup

Putting it all together, your Node server will handle routing, data prefetching, rendering, and HTML injection:

import express from 'express';
import path from 'path';
import createStore from '../shared/store';
import { matchRoutes } from 'react-router-dom';
import routes from '../shared/routes';
// ... other imports

const app = express();

app.use(express.static(path.resolve(__dirname, '../../dist')));

app.get('*', async (req, res) => {
  try {
    const store = createStore();

    // Prefetch data
    const matchingRoutes = matchRoutes(routes, req.path);
    const loadDataPromises = matchingRoutes
      .map(({ route }) => route.loadData?.(store))
      .filter(Boolean);

    await Promise.all(loadDataPromises);
    await store.sagaTask.toPromise();

    // Render app and collect chunks
    const extractor = new ChunkExtractor({ statsFile: path.resolve(__dirname, '../../dist/loadable-stats.json') });
    const appHtml = renderToString(
      extractor.collectChunks(
        <Provider store={store}>
          <StaticRouter location={req.path}>
            <App />
          </StaticRouter>
        </Provider>
      )
    );

    // Build final HTML
    const html = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>SSR App</title>
          ${extractor.getLinkTags()}
          ${extractor.getStyleTags()}
        </head>
        <body>
          <div id="root">${appHtml}</div>
          <script>window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())}</script>
          ${extractor.getScriptTags()}
        </body>
      </html>
    `;

    res.send(html);
  } catch (err) {
    res.status(500).send('Server error');
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

Key Pitfalls to Avoid

  • Hydration Mismatch: Always pass the exact server state to the client via window.__INITIAL_STATE__ to prevent React from throwing errors.
  • Saga Memory Leaks: Ensure sagas are cancelled or completed on the server before sending the response.
  • Webpack Targets: Use separate webpack configs for server (target: node) and client (target: web) to avoid bundling Node modules unnecessarily.

内容的提问来源于stack exchange,提问作者Fadi Quader

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 03:38:03