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

Module Federation SSR用React.lazy加载模块报useState空值错误求助

问题:React SSR中使用Module Federation时服务端报错"Cannot read properties of null (reading 'useState')"

现象

客户端通过React.lazy加载联邦模块完全正常,但服务端渲染时抛出错误:Cannot read properties of null (reading 'useState'),切换为UniversalFederationPlugin后问题依旧。

环境与代码

  • 依赖版本:
"@module-federation/node": "^0.9.11",
"react": "^18.2.0",
"webpack": "^5.74.0"
  • 模块导入方式:
const Homepage = React.lazy(() => import('homePage/Homepage'));

// 路由配置
{path: '/overview', element: <Homepage />},
  • 使用自定义React SSR服务。

相关配置

1. Shell应用客户端Webpack配置

{
    name: 'client',
    target: 'web',
    entry: {
        client: path.resolve(__dirname, '../../src/client/index.ts'),
    },
    output: {
        path: path.resolve(__dirname, '../../dist/client'),
        filename: 'js/[name].[contenthash].js',
        publicPath: '/static/',
        clean: true,
        assetModuleFilename: 'images/[hash][ext]',
    },
    optimization: {
        moduleIds: 'deterministic',
        minimize: !isDev ? true : false,
        minimizer: !isDev ? [new TerserPlugin()] : [],
    },
    module: {
        rules: [
            ...rules,
            {
                test: /\.s[ac]ss$/i,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader',
                ],
            },
        ],
    },
    plugins: [
        ...plugins.client,
    ],
};

2. Shell应用Webpack插件配置

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const {NodeFederationPlugin, StreamingTargetPlugin} = require('@module-federation/node');

const path = require('path');

const dependencies = require(path.join(__dirname, '../../../package.json')).dependencies;

const remotes = {
    server: {
        homePage: 'homePage@http://localhost:8080/server/homePageRemote.js',
    },
    client: {
        homePage: 'homePage@http://localhost:8080/client/homePageRemote.js',
    },
};

module.exports = {
    client: [
        new ModuleFederationPlugin({
            name: 'shellApp',
            remotes: {...remotes.client},
            shared: {
                react: {
                    singleton: true,
                    requiredVersion: dependencies['react'],
                },
                'react-dom': {
                    singleton: true,
                    requiredVersion: dependencies['react-dom'],
                },
            },
        }),
    ],
    server: [
        new NodeFederationPlugin({
            name: 'shellApp',
            library: {type: 'commonjs-module'},
            remotes: {...remotes.server},
            shared: {
                react: {
                    singleton: true,
                    requiredVersion: dependencies['react'],
                },
                'react-dom': {
                    singleton: true,
                    requiredVersion: dependencies['react-dom'],
                },
            },
        }),
        new StreamingTargetPlugin({
            name: 'shellApp',
            library: { type: 'commonjs-module' },
            remotes: {...remotes.server},
        }),
    ],
};

3. 远程应用服务端Webpack配置

const path = require('path');
const mainRules = require('../additionally/rules');
const { NodeFederationPlugin, StreamingTargetPlugin } = require('@module-federation/node');
const extensions = require('../additionally/extensions');

const mode = process.env.NODE_ENV === 'development' ? 'development' : 'production';
const isDev = mode === 'development';

const dependencies = require('../../package.json').dependencies;

module.exports = {
    entry: {
        server: path.join(__dirname, '../../src/index.tsx')
    },
    stats: {
        colors: true,
        entrypoints: false,
        children: false,
    },
    target: false,
    output: {
        path: path.join(__dirname, '../../dist/server'),
        filename: '[name].[contenthash].js',
        clean: true,
        publicPath: 'http://localhost:8080/server',
    },
    resolve: {
        extensions,
    },
    devtool: isDev ? 'inline-source-map' : 'source-map',
    module: {
        rules: [
            ...mainRules,
            {
                test: /\.(css|scss)$/i,
                    use: [
                    {
                        loader: "css-loader",
                    },
                    {
                        loader: "sass-loader",
                    },
                ],
                exclude: /node_modules/
            }
        ],
    },
    plugins: [
        new NodeFederationPlugin({
            name: 'homePage',
            filename: 'homePageRemote.js',
            library: { type: "commonjs-module" },
            remotes: {},
            exposes: {
                './Homepage': path.resolve(__dirname, '../../src/Homepage.tsx'),
            },
            shared: {
                react: {
                    singleton: true,
                    requiredVersion: dependencies['react'],
                },
                'react-dom': {
                    singleton: true,
                    requiredVersion: dependencies['react-dom'],
                }
            }
        }),
        new StreamingTargetPlugin({
            name: 'homePage',
            library: { type: "commonjs-module" },
            remotes: {},
            shared: {
                react: {
                    singleton: true,
                    requiredVersion: dependencies['react'],
                },
                'react-dom': {
                    singleton: true,
                    requiredVersion: dependencies['react-dom'],
                }
            }
        })
    ]
}

疑问

为什么React依赖没有被正确共享?


解答

这个错误本质是服务端渲染时React实例不共享,导致useState等钩子调用时找不到正确的React上下文。以下是核心原因和解决办法:

1. 核心原因

  • React.lazy不支持服务端渲染:React.lazy是客户端专用的懒加载方案,服务端无法处理异步加载的组件,会导致组件渲染时React上下文未正确初始化,出现null引用错误。
  • 服务端联邦模块的共享逻辑未完全生效:虽然配置了singleton: true,但服务端加载联邦模块时,可能因为模块解析顺序、Webpack配置细节问题,导致远程模块加载了自己的React副本,而非共享Shell的React实例。

2. 解决步骤

步骤1:替换React.lazy为服务端兼容的异步加载方案

使用React.Suspense配合服务端支持的组件加载方式,或者直接同步导入联邦模块(如果不需要懒加载):

// 替换React.lazy为同步导入(服务端友好)
import Homepage from 'homePage/Homepage';

// 路由配置
{path: '/overview', element: <Suspense fallback={<div>Loading...</div>}><Homepage /></Suspense>},

如果必须懒加载,可以使用@loadable/component替代React.lazy,它专门支持SSR场景:

import loadable from '@loadable/component';

const Homepage = loadable(() => import('homePage/Homepage'), {
  fallback: <div>Loading...</div>,
});

步骤2:确保服务端联邦配置的一致性

  • 检查shared配置的eager属性:在服务端的shared配置中添加eager: true,确保React在联邦模块加载前就被初始化,避免远程模块加载自己的副本:
// Shell服务端NodeFederationPlugin的shared配置
shared: {
    react: {
        singleton: true,
        requiredVersion: dependencies['react'],
        eager: true, // 添加此属性
    },
    'react-dom': {
        singleton: true,
        requiredVersion: dependencies['react-dom'],
        eager: true, // 添加此属性
    },
},
  • 远程应用服务端配置同步:远程应用的shared配置也需要添加eager: true,确保它优先使用Shell提供的React实例。

步骤3:验证服务端模块解析逻辑

  • 确保服务端Webpack配置的target设置正确(当前设置target: false在@module-federation/node中是允许的,但要确保没有其他冲突配置)。
  • 检查服务端入口文件是否正确初始化React上下文,确保在渲染联邦组件前,React已经被正确加载。

步骤4:升级依赖版本

@module-federation/node的旧版本存在一些SSR共享依赖的bug,尝试升级到最新稳定版,可能会解决此类兼容性问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 02:35:16