UI Federation Architecture
This document describes the UI federation architecture using React Native and Re.Pack for Module Federation, enabling cross-platform applications composed from multiple plugin UI modules.
Overview​
UPF uses Module Federation to dynamically compose UI from multiple plugins at runtime. This enables:
- Independent Deployment: Each plugin's UI can be updated without rebuilding the entire application
- Cross-Platform: Single codebase for Web, iOS, and Android via React Native
- Dynamic Loading: Load plugin UIs on demand based on installed plugins
- Shared Dependencies: Common libraries shared across all plugins to reduce bundle size
Re.Pack Module Federation​
What is Re.Pack?​
Re.Pack is a toolkit for React Native that provides Webpack-based bundling with Module Federation support. It enables:
- Webpack 5 Module Federation for React Native
- Code Splitting and lazy loading
- Dynamic Remote Loading at runtime
- Shared Dependencies between host and remotes
Architecture Components​
Host Configuration (UI Shell)​
// packages/ui-shell/webpack.config.js
const { withModuleFederation } = require('@callstack/repack/plugins');
module.exports = {
// ... base config
plugins: [
withModuleFederation({
name: 'host',
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '^18.0.0',
},
'react-native': {
singleton: true,
eager: true,
requiredVersion: '^0.72.0',
},
'@unified-plugin-framework/ui-sdk': {
singleton: true,
eager: true,
requiredVersion: '^1.0.0',
},
},
}),
],
};
Remote Configuration (Plugin)​
// plugins/auth-keycloak/frontend/webpack.config.js
const { withModuleFederation } = require('@callstack/repack/plugins');
module.exports = {
// ... base config
plugins: [
withModuleFederation({
name: 'authPlugin',
exposes: {
'./LoginScreen': './src/screens/LoginScreen',
'./ProfileWidget': './src/components/ProfileWidget',
'./useAuth': './src/hooks/useAuth',
'./AuthProvider': './src/providers/AuthProvider',
},
shared: {
react: {
singleton: true,
eager: false,
requiredVersion: '^18.0.0',
},
'react-native': {
singleton: true,
eager: false,
requiredVersion: '^0.72.0',
},
'@unified-plugin-framework/ui-sdk': {
singleton: true,
eager: false,
requiredVersion: '^1.0.0',
},
},
}),
],
};
UI Shell Architecture​
Shell Components​
Plugin Loader​
The Plugin Loader dynamically loads UI components from registered plugins:
// packages/ui-shell/src/plugin-loader.ts
import { Federated } from '@callstack/repack/client';
import type { PluginManifest, ExposedModule } from '@unified-plugin-framework/types';
interface LoadedPlugin {
id: string;
modules: Map<string, React.ComponentType>;
}
class PluginLoader {
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
private remoteContainers: Map<string, unknown> = new Map();
async loadPlugin(manifest: PluginManifest): Promise<LoadedPlugin> {
if (this.loadedPlugins.has(manifest.id)) {
return this.loadedPlugins.get(manifest.id)!;
}
const remoteUrl = this.resolveRemoteUrl(manifest);
// Initialize remote container
const container = await Federated.importModule(manifest.id, remoteUrl);
this.remoteContainers.set(manifest.id, container);
// Load exposed modules
const modules = new Map<string, React.ComponentType>();
for (const exposed of manifest.ui?.exposedModules ?? []) {
const module = await this.loadModule(manifest.id, exposed);
modules.set(exposed.name, module);
}
const loadedPlugin: LoadedPlugin = { id: manifest.id, modules };
this.loadedPlugins.set(manifest.id, loadedPlugin);
return loadedPlugin;
}
async loadModule(pluginId: string, exposed: ExposedModule): Promise<React.ComponentType> {
const module = await Federated.importModule(pluginId, exposed.name);
return module.default ?? module;
}
private resolveRemoteUrl(manifest: PluginManifest): string {
// In development: use dev server URL
// In production: use CDN or gateway URL
const baseUrl = process.env.PLUGIN_REGISTRY_URL;
return `${baseUrl}/plugins/${manifest.id}/remoteEntry.js`;
}
getLoadedModule(pluginId: string, moduleName: string): React.ComponentType | undefined {
return this.loadedPlugins.get(pluginId)?.modules.get(moduleName);
}
}
export const pluginLoader = new PluginLoader();
Dynamic Navigation​
// packages/ui-shell/src/navigation/DynamicNavigator.tsx
import React, { useEffect, useState } from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { usePluginRegistry } from '@unified-plugin-framework/ui-sdk';
import { pluginLoader } from '../plugin-loader';
const Stack = createStackNavigator();
interface DynamicRoute {
name: string;
component: React.ComponentType;
options?: object;
}
export function DynamicNavigator(): JSX.Element {
const { plugins } = usePluginRegistry();
const [routes, setRoutes] = useState<DynamicRoute[]>([]);
useEffect(() => {
async function loadRoutes(): Promise<void> {
const loadedRoutes: DynamicRoute[] = [];
for (const plugin of plugins) {
if (!plugin.ui?.navigation) continue;
const loaded = await pluginLoader.loadPlugin(plugin);
for (const nav of plugin.ui.navigation) {
const component = loaded.modules.get(nav.module);
if (component) {
loadedRoutes.push({
name: nav.path,
component,
options: { title: nav.title },
});
}
}
}
setRoutes(loadedRoutes);
}
loadRoutes();
}, [plugins]);
return (
<Stack.Navigator>
{routes.map((route) => (
<Stack.Screen
key={route.name}
name={route.name}
component={route.component}
options={route.options}
/>
))}
</Stack.Navigator>
);
}
State Management​
State Bridge​
The State Bridge enables controlled state sharing between plugins:
State Contract Definition​
// packages/core/types/ui-contracts.ts
import { z } from 'zod';
// Auth state contract - plugins can subscribe to this
export const AuthStateSchema = z.object({
isAuthenticated: z.boolean(),
user: z
.object({
id: z.string(),
email: z.string(),
displayName: z.string(),
avatarUrl: z.string().optional(),
roles: z.array(z.string()),
})
.nullable(),
token: z.string().nullable(),
expiresAt: z.number().nullable(),
});
export type IAuthState = z.infer<typeof AuthStateSchema>;
// Notification state contract
export const NotificationStateSchema = z.object({
notifications: z.array(
z.object({
id: z.string(),
type: z.enum(['info', 'success', 'warning', 'error']),
title: z.string(),
message: z.string(),
timestamp: z.number(),
read: z.boolean(),
}),
),
unreadCount: z.number(),
});
export type INotificationState = z.infer<typeof NotificationStateSchema>;
// Shared state contract declaration in manifest
export interface SharedStateContract {
name: string;
schema: z.ZodSchema;
readonly: boolean;
owner: string; // Plugin that owns this state
}
State Bridge Implementation​
// packages/ui-sdk/src/state-bridge.ts
import { createContext, useContext, useSyncExternalStore } from 'react';
import type { SharedStateContract } from '@unified-plugin-framework/types';
type StateListener = () => void;
class StateBridge {
private state: Map<string, unknown> = new Map();
private contracts: Map<string, SharedStateContract> = new Map();
private listeners: Map<string, Set<StateListener>> = new Map();
registerContract(contract: SharedStateContract): void {
this.contracts.set(contract.name, contract);
this.listeners.set(contract.name, new Set());
}
setState<T>(name: string, value: T, pluginId: string): void {
const contract = this.contracts.get(name);
if (!contract) {
throw new Error(`Unknown state contract: ${name}`);
}
if (contract.readonly && contract.owner !== pluginId) {
throw new Error(`State ${name} is readonly for plugin ${pluginId}`);
}
// Validate against schema
const result = contract.schema.safeParse(value);
if (!result.success) {
throw new Error(`Invalid state for ${name}: ${result.error.message}`);
}
this.state.set(name, value);
this.notifyListeners(name);
}
getState<T>(name: string): T | undefined {
return this.state.get(name) as T | undefined;
}
subscribe(name: string, listener: StateListener): () => void {
const listeners = this.listeners.get(name);
if (!listeners) {
throw new Error(`Unknown state contract: ${name}`);
}
listeners.add(listener);
return () => listeners.delete(listener);
}
private notifyListeners(name: string): void {
const listeners = this.listeners.get(name);
listeners?.forEach((listener) => listener());
}
}
export const stateBridge = new StateBridge();
// React hook for using shared state
export function useSharedState<T>(name: string): T | undefined {
return useSyncExternalStore(
(callback) => stateBridge.subscribe(name, callback),
() => stateBridge.getState<T>(name),
);
}
// Hook for setting shared state
export function useSetSharedState<T>(name: string, pluginId: string) {
return (value: T) => stateBridge.setState(name, value, pluginId);
}
Event Bus​
Cross-Plugin Events​
Event Bus Implementation​
// packages/ui-sdk/src/event-bus.ts
type EventHandler<T = unknown> = (data: T) => void;
interface EventSubscription {
unsubscribe: () => void;
}
class EventBus {
private handlers: Map<string, Set<EventHandler>> = new Map();
emit<T>(event: string, data: T): void {
const handlers = this.handlers.get(event);
handlers?.forEach((handler) => {
try {
handler(data);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
// Also emit to wildcard listeners
const wildcardHandlers = this.handlers.get('*');
wildcardHandlers?.forEach((handler) => {
try {
handler({ event, data });
} catch (error) {
console.error(`Error in wildcard handler for ${event}:`, error);
}
});
}
on<T>(event: string, handler: EventHandler<T>): EventSubscription {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler as EventHandler);
return {
unsubscribe: () => {
this.handlers.get(event)?.delete(handler as EventHandler);
},
};
}
once<T>(event: string, handler: EventHandler<T>): EventSubscription {
const wrappedHandler: EventHandler<T> = (data) => {
subscription.unsubscribe();
handler(data);
};
const subscription = this.on(event, wrappedHandler);
return subscription;
}
}
export const eventBus = new EventBus();
// React hook
export function useEventBus() {
return eventBus;
}
export function useEvent<T>(event: string, handler: EventHandler<T>): void {
useEffect(() => {
const subscription = eventBus.on(event, handler);
return () => subscription.unsubscribe();
}, [event, handler]);
}
API Communication​
gRPC-Web Client​
// packages/ui-sdk/src/grpc-client.ts
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport';
import type { RpcTransport } from '@protobuf-ts/runtime-rpc';
interface GrpcClientConfig {
baseUrl: string;
getAuthToken: () => string | null;
}
export function createGrpcTransport(config: GrpcClientConfig): RpcTransport {
return new GrpcWebFetchTransport({
baseUrl: config.baseUrl,
meta: () => {
const token = config.getAuthToken();
return token ? { Authorization: `Bearer ${token}` } : {};
},
});
}
// Usage in plugin
import { OrderServiceClient } from './generated/order.client';
const transport = createGrpcTransport({
baseUrl: 'https://api.example.com',
getAuthToken: () => authState.token,
});
const orderClient = new OrderServiceClient(transport);
// Make calls
const { response } = await orderClient.getOrder({ id: 'order-123' });
WebSocket for Real-time​
// packages/ui-sdk/src/websocket-client.ts
import { useEffect, useState, useCallback } from 'react';
interface WebSocketConfig {
url: string;
getAuthToken: () => string | null;
reconnectInterval?: number;
}
export function useWebSocket<T>(config: WebSocketConfig, onMessage: (data: T) => void) {
const [connected, setConnected] = useState(false);
const [socket, setSocket] = useState<WebSocket | null>(null);
useEffect(() => {
const token = config.getAuthToken();
const wsUrl = `${config.url}?token=${token}`;
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
setConnected(true);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data) as T;
onMessage(data);
};
ws.onclose = () => {
setConnected(false);
// Reconnect logic
setTimeout(() => {
// Reconnect
}, config.reconnectInterval ?? 5000);
};
setSocket(ws);
return () => {
ws.close();
};
}, [config.url]);
const send = useCallback(
(data: unknown) => {
socket?.send(JSON.stringify(data));
},
[socket],
);
return { connected, send };
}
Build and Deployment​
Federated Build Process​
Build Configuration​
// tools/build-service/src/federated-builder.ts
interface BuildConfig {
plugins: PluginManifest[];
platform: 'web' | 'ios' | 'android';
environment: 'development' | 'production';
}
async function buildFederatedApp(config: BuildConfig): Promise<BuildResult> {
// 1. Download plugin UI sources
for (const plugin of config.plugins) {
await downloadPluginSource(plugin);
}
// 2. Generate federation config
const federationConfig = generateFederationConfig(config.plugins);
// 3. Build host application
const hostBundle = await buildHost(federationConfig, config.platform);
// 4. Build plugin remotes
const remotes = await Promise.all(config.plugins.map((p) => buildPluginRemote(p, config.platform)));
// 5. Combine outputs
return {
host: hostBundle,
remotes,
manifest: generateRuntimeManifest(config.plugins),
};
}
Runtime Loading​
Cross-Platform Considerations​
Platform-Specific Components​
// Plugin can provide platform-specific implementations
// plugins/my-plugin/frontend/src/components/CameraButton/index.ts
import { Platform } from 'react-native';
export const CameraButton = Platform.select({
web: () => require('./CameraButton.web').default,
ios: () => require('./CameraButton.ios').default,
android: () => require('./CameraButton.android').default,
})!;
Responsive Design​
// packages/ui-sdk/src/responsive.ts
import { useWindowDimensions } from 'react-native';
export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export function useBreakpoint(): Breakpoint {
const { width } = useWindowDimensions();
if (width < 576) return 'xs';
if (width < 768) return 'sm';
if (width < 992) return 'md';
if (width < 1200) return 'lg';
return 'xl';
}
export function useResponsiveValue<T>(values: Partial<Record<Breakpoint, T>>): T {
const breakpoint = useBreakpoint();
const breakpoints: Breakpoint[] = ['xs', 'sm', 'md', 'lg', 'xl'];
// Find the closest smaller breakpoint with a value
const currentIndex = breakpoints.indexOf(breakpoint);
for (let i = currentIndex; i >= 0; i--) {
const value = values[breakpoints[i]];
if (value !== undefined) return value;
}
throw new Error('No responsive value found');
}
Related Documentation​
- Architecture Overview - High-level system architecture
- Communication - Backend communication patterns
- UI Contracts - Frontend state specifications
- Plugin Development - Building plugin UIs
Previous: Communication Architecture Next: Deployment Architecture