React Bridge
The @gauzy/ui-react package enables embedding React components inside the Angular host application. It provides directives for mounting React trees and hooks for accessing Angular services from React code.
Architecture
Diagram source
graph TD
subgraph Angular["Angular Host"]
Directive["ReactHostDirective"]
Injector["Angular Injector"]
end
subgraph React["React Tree"]
Provider["NgContextProvider"]
ErrorBoundary["PluginErrorBoundary"]
Hook1["useInjector()"]
Hook2["useTranslation()"]
Hook3["usePluginSettings()"]
Hook4["usePluginEvents()"]
Component["React Component"]
end
Directive --> Provider
Provider --> ErrorBoundary
ErrorBoundary --> Component
Component --> Hook1
Component --> Hook2
Component --> Hook3
Component --> Hook4
Hook1 --> InjectorThe ReactHostDirective creates a React root inside an Angular element, wraps the React tree with NgContextProvider (which exposes the Angular Injector) and PluginErrorBoundary (which isolates crashes), then hooks allow React components to consume Angular services.
Mounting React Components
ReactHostDirective
Renders a React component inside an Angular template.
@Directive({ selector: '[gaReactHost]', standalone: true })
export class ReactHostDirective {
@Input({ required: true }) gaReactHost!: React.ComponentType<unknown>;
@Input() props: Record<string, unknown> = {};
@Input() context: Record<string, unknown> = {};
}Usage:
import { Component } from '@angular/core';
import { ReactHostDirective } from '@gauzy/ui-react';
import { MyReactComponent } from './my-react-component';
@Component({
selector: 'my-angular-wrapper',
standalone: true,
imports: [ReactHostDirective],
template: `<div [gaReactHost]="component" [props]="props"></div>`
})
export class MyAngularWrapperComponent {
component = MyReactComponent;
props = { title: 'Hello from Angular' };
}LazyReactHostDirective
Lazy-loads a React component via dynamic import (code-splitting). Component is loaded only when the directive is rendered.
@Directive({ selector: '[gaReactLazyHost]', standalone: true })
export class LazyReactHostDirective {
@Input({ required: true })
gaReactLazyHost!: () => Promise<{ default: React.ComponentType<unknown> }>;
@Input() props: Record<string, unknown> = {};
@Input() context: Record<string, unknown> = {};
}Usage:
<div [gaReactLazyHost]="loadComponent" [props]="props"></div>loadComponent = () => import('./heavy-component').then(m => m);
props = { columns: 3 };ReactBridge (Programmatic)
For full control, use the ReactBridge class directly:
import { ReactBridge, provideReactBridge, REACT_BRIDGE } from '@gauzy/ui-react';
// Register provider
providers: [provideReactBridge()]
// In component
const bridge = inject(REACT_BRIDGE);
const result = bridge.mount({
component: MyReactComponent,
props: { title: 'Hello' },
hostElement: this.elementRef.nativeElement,
injector: this.injector
});
// Update props
result.updateProps({ title: 'Updated!' });
// Cleanup
result.unmount();| Export | Description |
|---|---|
ReactBridge | Bridge class with mount(), isCompatible() |
createReactBridge() | Factory function |
provideReactBridge() | Angular provider helper (EnvironmentProviders) |
REACT_BRIDGE | Injection token |
Context & Providers
NgContextProvider
React context provider that exposes Angular's Injector to child components. Automatically wraps children in PluginErrorBoundary.
interface NgContextProviderProps {
injector: Injector;
context?: Record<string, unknown>;
children?: React.ReactNode;
pluginId?: string;
errorFallback?: React.ReactNode | ((info: PluginErrorInfo, retry: () => void) => React.ReactNode);
onError?: (info: PluginErrorInfo) => void;
}useBridgeContext
Access the full bridge context (injector + extra context values):
function useBridgeContext(): NgReactBridgeContext
// { injector: Injector, [key: string]: unknown }Hooks Reference
All hooks must be called inside React components mounted via the bridge (within NgContextProvider).
useInjector
Access the Angular Injector or retrieve a specific service directly.
// Overload 1: Get the injector
function useInjector(): Injector;
// Overload 2: Get a specific service by token
function useInjector<T>(token: ProviderToken<T>): T;Examples:
// Get the injector for multiple lookups
const injector = useInjector();
const http = injector.get(HttpClient);
const router = injector.get(Router);
// Get a specific service directly
const myService = useInjector(MyAngularService);Throws: Error if not used within NgContextProvider.
useObservable
Subscribe to an RxJS Observable and get the latest value as React state. Handles BehaviorSubject synchronously (no flash of initial value).
// With initial value — returns T
function useObservable<T>(observable$: Observable<T>, initialValue: T): T;
// Without initial value — returns T | undefined
function useObservable<T>(observable$: Observable<T>): T | undefined;Features:
- Synchronous value reading from
BehaviorSubject - Auto-resubscription when observable reference changes
- Automatic cleanup on unmount
Example:
const injector = useInjector();
const store = injector.get(Store);
const user = useObservable(store.user$, null);
return <div>Hello, {user?.name}</div>;useTranslation
Reactive translations via PLUGIN_TRANSLATE_SERVICE. Re-renders on language change.
// Overload 1: Get translation function + language
function useTranslation(): { t: (key: string, params?: Record<string, unknown>) => string; lang: string };
// Overload 2: Translate a single key directly
function useTranslation(key: string, params?: Record<string, unknown>): string;Examples:
// Multi-key usage
const { t, lang } = useTranslation();
return (
<div>
<h1>{t('MY_PLUGIN.TITLE', { user: 'Alice' })}</h1>
<p>Language: {lang}</p>
</div>
);
// Single-key usage (convenience)
const title = useTranslation('MY_PLUGIN.TITLE');
const greeting = useTranslation('MY_PLUGIN.GREETING', { name: 'World' });usePluginSettings
Read all settings for a plugin reactively.
function usePluginSettings(pluginId: string): Record<string, unknown>Example:
const settings = usePluginSettings('time-tracker');
const autoStart = settings['autoStart'] as boolean ?? false;usePluginSetting
Read a single setting value with optional default.
// With default — returns T
function usePluginSetting<T>(pluginId: string, key: string, defaultValue: T): T;
// Without default — returns T | undefined
function usePluginSetting<T>(pluginId: string, key: string): T | undefined;Example:
const refreshInterval = usePluginSetting<number>('my-plugin', 'refreshInterval', 300);
const theme = usePluginSetting<string>('my-plugin', 'theme', 'light');usePluginState
Reactive access to the global plugin state store. Returns [value, setValue] tuple like React's useState, backed by PluginStateService.
// With initial value — returns [T, setter]
function usePluginState<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void];
// Without initial value — returns [T | undefined, setter]
function usePluginState<T>(key: string): [T | undefined, (value: T | ((prev: T | undefined) => T)) => void];Features:
- Multi-source updates: changes from Angular, other React components, or other plugins trigger re-render
- Supports updater functions like React's
useState - Auto-initializes with
initialValueif key doesn't exist
Convention: Prefix keys with your plugin ID to avoid collisions (e.g., 'my-plugin:counter').
Example:
const [count, setCount] = usePluginState<number>('my-plugin:count', 0);
const [items, setItems] = usePluginState<string[]>('my-plugin:items', []);
// Direct value
setCount(42);
// Updater function
setItems(prev => [...prev, 'new item']);usePluginEvents
Raw access to the plugin event bus for inter-plugin communication.
function usePluginEvents(pluginId?: string): {
emit: <T>(type: string, payload: T, options?: EmitOptions) => void;
on: <T>(type: string, options?: SubscribeOptions) => Observable<PluginEvent<T>>;
onPattern: <T>(pattern: string, options?: SubscribeOptions) => Observable<PluginEvent<T>>;
once: <T>(type: string, callback: (event: PluginEvent<T>) => void, options?: SubscribeOptions) => void;
}Example:
const events = usePluginEvents('my-plugin');
useEffect(() => {
const sub = events.on<{ duration: number }>('time-tracked').subscribe(event => {
console.log('Duration:', event.payload.duration);
});
return () => sub.unsubscribe();
}, [events]);
// Emit an event
events.emit('widget-clicked', { widgetId: 'stats' });usePluginEvent
Subscribe to a specific event type with automatic cleanup.
function usePluginEvent<T>(
type: string,
callback: (event: PluginEvent<T>) => void,
options?: SubscribeOptions,
deps?: React.DependencyList
): voidExample:
usePluginEvent('time-tracked', (event) => {
console.log('Time tracked:', event.payload);
});useTypedEvent
Type-safe event handling using event contracts defined with definePluginEvent().
function useTypedEvent<T>(contract: PluginEventContract<T>): TypedEventHandle<T>
// TypedEventHandle<T> = { emit(payload: T): void; on(): Observable<PluginEvent<T>> }Example:
import { DashboardRefreshedEvent } from '@gauzy/plugin-dashboard-time-track-react-ui';
const handle = useTypedEvent(DashboardRefreshedEvent);
// Emit (fully typed — TypeScript checks the payload shape)
handle.emit({ timestamp: Date.now() });
// Subscribe (fully typed — event.payload is typed)
useEffect(() => {
const sub = handle.on().subscribe(event => {
console.log('Refreshed at', event.payload.timestamp);
});
return () => sub.unsubscribe();
}, [handle]);useTypedEventListener
Subscribe to a typed event with automatic cleanup (no manual unsubscribe needed).
function useTypedEventListener<T>(
contract: PluginEventContract<T>,
callback: (event: PluginEvent<T>) => void,
options?: SubscribeOptions,
deps?: React.DependencyList
): voidExample:
useTypedEventListener(DashboardRefreshedEvent, (event) => {
console.log('Duration:', event.payload.timestamp);
});useDynamicPlugin
Dynamically load/unload plugins at runtime from React.
function useDynamicPlugin(): {
loadedIds: string[];
load: (definition: PluginUiDefinition) => Promise<DynamicPluginLoadResult>;
unload: (pluginId: string) => Promise<DynamicPluginLoadResult>;
reload: (definition: PluginUiDefinition) => Promise<DynamicPluginLoadResult>;
isLoaded: (pluginId: string) => boolean;
loading: boolean;
lastResult: DynamicPluginLoadResult | null;
}Example:
const { loadedIds, load, unload, isLoaded, loading } = useDynamicPlugin();
const handleInstall = async () => {
const result = await load({
id: 'analytics',
bootstrap: (injector) => { /* ... */ }
});
if (!result.success) alert(result.error);
};
return (
<div>
<p>Active: {loadedIds.join(', ')}</p>
<button onClick={handleInstall} disabled={loading}>Install</button>
<button onClick={() => unload('analytics')}>Remove</button>
</div>
);React Extensions in Slots
Register React components as extension slot contributions using helper functions.
defineReactExtension (Eager)
function defineReactExtension<TProps>(config: ReactExtensionConfig<TProps>): ReactExtensionDefinition<TProps>interface ReactExtensionConfig<TProps> {
id: string;
slotId: string;
component: React.ComponentType<TProps>;
props?: TProps | (() => TProps);
context?: Record<string, unknown>;
order?: number;
// Visibility
permissions?: string[];
permissionsAny?: string[];
featureKey?: string;
visible?: (context: ReactExtensionVisibilityContext) => boolean | Promise<boolean>;
hidden?: boolean;
// Wrapper
wrapper?: 'none' | 'card' | 'widget' | 'window' | 'panel'
| { type: string; title?: string; cssClass?: string; showHeader?: boolean };
// Lifecycle
onMount?: (context: ReactExtensionLifecycleContext) => void | Promise<void>;
onUnmount?: (context: ReactExtensionLifecycleContext) => void | Promise<void>;
onActivate?: (context: ReactExtensionLifecycleContext) => void | Promise<void>;
onDeactivate?: (context: ReactExtensionLifecycleContext) => void | Promise<void>;
// Metadata
metadata?: { title?: string; description?: string; icon?: string; category?: string; tags?: string[] };
}Example:
export const MyPlugin = defineDeclarativePlugin('my-plugin', {
extensions: [
defineReactExtension({
id: 'my-plugin:react-widget',
slotId: 'dashboard-page',
component: MyWidget,
order: 10,
wrapper: 'card',
permissions: ['ADMIN_DASHBOARD_VIEW'],
onMount: (ctx) => console.log('Widget mounted in', ctx.slotId)
})
]
});defineLazyReactExtension (Code-Splitting)
function defineLazyReactExtension<TProps>(config: LazyReactExtensionConfig<TProps>): ReactExtensionDefinition<TProps>
// Same as ReactExtensionConfig but replaces `component` with:
interface LazyReactExtensionConfig<TProps> {
loadComponent: () => Promise<React.ComponentType<TProps>>;
// ... all other fields same as ReactExtensionConfig
}Example:
defineLazyReactExtension({
id: 'my-plugin:heavy-widget',
slotId: 'dashboard-page',
loadComponent: () => import('./heavy-widget').then(m => m.HeavyWidget),
order: 20
})isReactExtension (Type Guard)
function isReactExtension(ext: unknown): ext is ReactExtensionDefinitionError Boundary
React components are automatically wrapped in PluginErrorBoundary by NgContextProvider. You can also use it manually for finer-grained isolation.
interface PluginErrorInfo {
error: Error;
componentStack?: string;
pluginId?: string;
}
interface PluginErrorBoundaryProps {
children?: ReactNode;
pluginId?: string;
fallback?: ReactNode | ((info: PluginErrorInfo, retry: () => void) => ReactNode);
onError?: (info: PluginErrorInfo) => void;
}Examples:
// Simple fallback
<PluginErrorBoundary pluginId="charts" fallback={<p>Chart failed to load</p>}>
<ChartWidget />
</PluginErrorBoundary>
// Render function with retry
<PluginErrorBoundary
pluginId="stats"
fallback={(info, retry) => (
<div>
<p>Error: {info.error.message}</p>
<button onClick={retry}>Retry</button>
</div>
)}
onError={(info) => logToSentry(info)}
>
<StatsWidget />
</PluginErrorBoundary>Default fallback: styled error box with error message and "Retry" button.
Webpack Configuration
To support JSX in the Angular build pipeline, a custom webpack rule is included in apps/gauzy/config/custom-webpack.config.js:
{
test: /\.jsx$/,
exclude: /node_modules/,
use: [{
loader: 'ts-loader',
options: {
transpileOnly: true,
compilerOptions: { jsx: 'react-jsx', allowJs: true }
}
}]
}TSX files are handled by the existing TypeScript loader with jsx: 'react-jsx' in the relevant tsconfig.json.
Related Pages
- React Components Library — Card, Progress, ColorDots, theme tokens
- Extension Slots — registering components in slots
- Plugin Services — events and settings from React
- Getting Started — your first plugin