Vite - React TypeError: Cannot read properties of null (reading 'useState')

Hi, I'm having an error when I try to load a microfrontend with Vite and module federation plugin. react and react-dom versions are aligned on version 19, and versions of parent UI and MFE's react are aligned as well

React TypeError: Cannot read properties of null (reading 'useState')

How can I fix this?

Solution

This is probably because the two instances of react are loaded, one is from the remote MFE and the other one is from the host loading the MFE. You can fix it by updating Vite configuration and setting singleton: true. It is also highly suggested to add requiredVersion parameter to guarantee that the version of the MFE is compatible with the host UI

Here is an example of vite.config.ts file

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { federation } from "@module-federation/vite";
import { dependencies } from "./package.json";

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "remote_app_name",
      filename: "remoteEntry.js",
      exposes: {
        "./ExposedComponent": "./src/components/ExposedComponent.tsx",
      },
      shared: {
        react: { requiredVersion: dependencies.react, singleton: true },
        "react-dom": {
          requiredVersion: dependencies["react-dom"],
          singleton: true,
        },
        "react-router": {
          requiredVersion: dependencies["react-router"],
          singleton: true,
        },
      },
    }),
  ],
  build: {
    target: "esnext",
    rollupOptions: {
      output: {
        assetFileNames() {
          return "[name][extname]";
        },
      },
    },
  },
});

Optionally, instead of setting requiredVersion, you can try setting import: false, this will guarantee that react dependency is packaged in the remote MFE build, forcing the module to use the host react version

Alternative #1

I've been dealing with Vite module federation issues, and sometimes the singleton configuration needs to be more explicit. One approach that's worked well for me is using external dependencies to completely exclude React from the remote bundle:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { federation } from "@module-federation/vite";
import { dependencies } from "./package.json";

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "remote_app_name",
      filename: "remoteEntry.js",
      exposes: {
        "./ExposedComponent": "./src/components/ExposedComponent.tsx",
      },
      shared: {
        react: { 
          requiredVersion: dependencies.react, 
          singleton: true,
          import: false, // Don't import React in remote
        },
        "react-dom": {
          requiredVersion: dependencies["react-dom"],
          singleton: true,
          import: false,
        },
      },
    }),
  ],
  build: {
    target: "esnext",
    rollupOptions: {
      external: ['react', 'react-dom'], // Mark as external
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
});

This approach ensures the remote app doesn't bundle React at all and relies entirely on the host's React instance.

Alternative #2

Another common issue I've encountered is version mismatches between host and remote apps. You can use a more strict version checking approach:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { federation } from "@module-federation/vite";

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "remote_app_name",
      filename: "remoteEntry.js",
      exposes: {
        "./ExposedComponent": "./src/components/ExposedComponent.tsx",
      },
      shared: {
        react: { 
          singleton: true,
          strictVersion: true, // Fail if versions don't match exactly
          requiredVersion: "19.0.0",
        },
        "react-dom": {
          singleton: true,
          strictVersion: true,
          requiredVersion: "19.0.0",
        },
      },
    }),
  ],
  build: {
    target: "esnext",
    rollupOptions: {
      output: {
        assetFileNames() {
          return "[name][extname]";
        },
      },
    },
  },
});

This will cause the build to fail if there are any version mismatches, helping you catch issues early.

Alternative #3

If you're still having issues, the problem might be bundling order or module resolution. You can try using a bootstrap approach in your host application:

// In your host app's main entry point
import React from 'react';
import ReactDOM from 'react-dom';

// Ensure React is available globally before loading remotes
window.React = React;
window.ReactDOM = ReactDOM;

// Then load your remote modules
import('./remote-app/remoteEntry.js').then(() => {
  // Remote is now loaded and React is available
});

Or use dynamic imports with preloading:

// In your host app
const loadRemoteApp = async () => {
  // Preload React dependencies
  await Promise.all([
    import('react'),
    import('react-dom')
  ]);
  
  // Then load remote
  const remote = await import('remote-app/remoteEntry.js');
  return remote;
};

This gives you more control over the loading sequence and can help resolve timing-related React availability issues.

Last modified: March 30, 2025