Skip to content

Dynamic approach of loading remotes not working with module-federation/vite & module-federation/enhanced packages #4404

@maverick09

Description

@maverick09

Approach: Dynamic module federation setup using following packages

"@module-federation/enhanced": "^0.19.1",
 "@module-federation/vite": "^1.8.1",


  1. I am creating instance using createInstance as first thing when app loads
  2. Take the instance object and using it to later register and loadRemote
  3. Ideally this should work but I keep getting error Please call createInstance first on first page load

Error: Please call createInstance first. Error Code: RUNTIME-009** for first page load**, after that it loads fine without the error.
4. Its failing at this line when trying to loadRemote

5. const module = await  this.fedInstance.loadRemote<T>(`${remoteName}/${exposedModule}`);
			if (!module) {
				throw new Error(`Module ${remoteName}/${exposedModule} is null or undefined`);
			}
			return module as NonNullable<T>;
  1. I have looked into global instances created. On page load it creates instance of FederationHost with my remotes in it.
  2. When it tries to load RemoteApp it creates another instances ModuleFederation
  3. Then it throws error
  4. ** What could be the reason of this error in below setup ?**
  5. I have tried with different versions of these packages using old init function as well but in the end it leads to same behaviour.

Appreciate any suggestions to fix this issue

SETUP:

//1. Main.tsx

import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import { createInstance } from '@module-federation/enhanced/runtime';
import { BrowserRouter } from 'react-router-dom';

// Create federation instance
export const federationInstance = createInstance({
	name: 'shell',
	remotes: [],
	shared: {
		react: {
			version: '18.3.1',
			scope: 'default',
			lib: () => import('react'),
			shareConfig: {
				singleton: true,
				eager: true,
				requiredVersion: '>=18.3.1',
				strictVersion: false,
			},
		},
		'react-dom': {
			version: '18.3.1',
			scope: 'default',
			lib: () => import('react-dom'),
			shareConfig: {
				singleton: true,
				eager: true,
				requiredVersion: '>=18.3.1',
				strictVersion: false,
			},
		},
		'react-router-dom': {
			version: '7.6.0',
			scope: 'default',
			lib: () => import('react-router-dom'),
			shareConfig: {
				singleton: true,
				eager: true,
				requiredVersion: '~7.6.0',
				strictVersion: false,
			},
		},
	},
});

FrontendService.initialise(federationInstance); /

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<LandingPage />)

// 2. FrontendService.ts


**import { loadRemote, registerRemotes, getInstance } from '@module-federation/enhanced/runtime';**
import React from 'react';
import { lazy } from 'react';
import { RouteObject } from 'react-router-dom';

export interface MicroFrontendConfig {
	name: string;
	url: string;
	exposedModule: string;
	routePath: string;
	moduleName?: string;
	routeData?: any;
}

export class FrontendService {
	private static registeredRemotes = new Set<string>();
	private static isInitialized = false;
        private static fedInstance ;

	
	static async initialise( instance ) {
               this.fedInstance = instance;
	}

	
	static async registerRemote(config: MicroFrontendConfig): Promise<void> {
		// Ensure federation is initialized
		await this.ensureInitialized();

		if (this.registeredRemotes.has(config.name)) {
			return;
		}

		await  this.fedInstance.registerRemotes([
			{
				name: config.name,
				entry: config.url,
				alias: config.name,
				type: 'module',
			},
		]);

		this.registeredRemotes.add(config.name);
	}

	/**
	 * Load a remote module dynamically
	 */
	static async loadRemoteModule<T = any>(
		remoteName: string,
		exposedModule: string,
	): Promise<NonNullable<T>> {

		try {
			const module = await  this.fedInstance.loadRemote<T>(`${remoteName}/${exposedModule}`);
			if (!module) {
				throw new Error(`Module ${remoteName}/${exposedModule} is null or undefined`);
			}
			return {
				default: module[moduleName] || module.default || module,
			};
		} catch (error) {
			console.error(`Failed to load remote module ${remoteName}/${exposedModule}:`, error);
			throw error;
		}
	}

	
	static createRemoteComponent(config: MicroFrontendConfig): React.ComponentType<any> {
		const moduleName = config.moduleName || 'default';

		return lazy(async () => {
			await this.registerRemote(config);

			const component= await this.loadRemoteModule(config.name, config.exposedModule || './App');

			return component;
		});
	}

	
	static setupMicroFrontendRoute(config: MicroFrontendConfig): RouteObject {
		const RemoteComponent = this.createRemoteComponent(config);

		return {
			path: config.routePath,
			element: React.createElement(RemoteComponent),
			// Pass any additional route data
			...(config.routeData ? { handle: config.routeData } : {}),
		};
	}

	
	static createMicroFrontendRoutes(configs: MicroFrontendConfig[]): RouteObject[] {
		return configs.map((config) => this.setupMicroFrontendRoute(config));
	}

	
	static async preloadRemoteEntries(configs: MicroFrontendConfig[]): Promise<void[]> {
		// Ensure federation is initialized first
		return Promise.all(configs.map((config) => this.registerRemote(config)));
	}
}

// 3. LandingPage.tsx

import { lazy, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Suspense, useEffect, useState } from 'react';
import {	createBrowserRouter,	Link,	Navigate,	Outlet,	RouteObject,	RouterProvider,	useLocation,} from 'react-router-dom';
import { FrontendConfig, FrontendService } from '../services/FrontendService';

export function LandingPage() {
	const [router, setRouter] = useState<ReturnType<typeof createBrowserRouter> | null>(null);
	const [loading, setLoading] = useState(true);

	useEffect(() => {
		const initializeMicroFrontends = async () => {
			try {
				
				const microFrontends: MicroFrontendConfig[] = [
					{
						name: 'remoteApp',
						url: 'http://localhost:5050/remoteEntry.js',
						exposedModule: 'RemoteMarketApp',
						moduleName: 'RemoteMarketApp',
						routePath: '/remote-market/*',
						routeData: { title: remote-market' },
					},
				];

				**// Preload remote entries for better performance
				await FrontendService.preloadRemoteEntries(microFrontends);

				// Create dynamic routes
				const dynamicRoutes = FrontendService.createMicroFrontendRoutes(microFrontends);**

				// Define all routes
				const routes: RouteObject[] = [
					{
						path: '/',
						element: <Layout />, // Layout with nav and outlet
						children: [
							{ path: '/', element: <Navigate to="/remote-market" replace /> },
							{ path: 'profile', element: <Profile /> },
							...dynamicRoutes,							
						],
					},
				];

				// Create router
    			        const browserRouter = createBrowserRouter(routes);
				setRouter(browserRouter);

			} catch (error) {
				console.error('Failed to initialize micro frontends:', error);
			} finally {
				setLoading(false);
			}
		};
		initializeMicroFrontends();
	}, []);

	if (loading || !router) {
		return (
			<div >
				<div>Loading micro frontends...</div>
			</div>
		);
	}
	return (
		<div>
			<div >
				<h3>UI : Shell</h3>
				<h6>Shell container for UI application</h6>
				<Suspense fallback={<div>Loading app...</div>}>
					<ErrorBoundary>
						<RouterProvider router={router} />
					</ErrorBoundary>
				</Suspense>
			</div>
		</div>
	);
}


//**vite.config.ts**
import { defineConfig } from 'vite';
export default defineConfig(({ mode }) => ({
	root: __dirname,
	
	server: {
		port: 1010,
		host: 'localhost',
		fs: {
			strict: false,
		},
	},
     }
   )
);



// REMOTE MARKET APP's **vite.config.ts**
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { federation } from '@module-federation/vite';
export default defineConfig(({ mode }) => {
	return {
		root: __dirname,

		server: {
			port: 5050,
			host: 'localhost',
			cors: true,
		},

		plugins: [
			react(),
			federation({
				name: 'remoteApp',
				filename: 'remoteEntry.js',
				exposes: {
					'./RemoteMarketApp': './src/remote/RemoteMarketApp.tsx',
				},
				shared: {
					react: {
						singleton: true,
						requiredVersion: '^18.3.1',
						strictVersion: false,
					},
					'react-dom': {
						singleton: true,
						requiredVersion: '^18.3.1',
						strictVersion: false,
					},
					'react-router-dom': {
						singleton: true,
						requiredVersion: '^7.6.0',
						strictVersion: false,
					},
				},
			}),
		],
	};
    }
);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions