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β
The 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
): void
Example:
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
): void
Example:
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 ReactExtensionDefinition
Error 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