React micro-frontend that runs alongside the Angular host via Webpack Module Federation.
The Angular host's src/app/share/react-mount/ exports two pieces:
ReactRemoteLoaderService — loads remoteEntry.js once per page, caches per-module promises, evicts on error.ReactMountDirective — owns the host element, mounts outside the Angular zone, forwards [reactProps] changes through handle.update(...), and unmounts on destroy. Re-checks destroyed after the async load so a navigation during load does not leak a mount.Both are exported from ShareModule. Any notebook or interpreter template can use the directive without additional wiring. The selector is kebab-case (zeppelin-react-mount) per project ESLint convention.
| Phase | Scope | Status |
|---|---|---|
| 1 | Webpack 5 + Module Federation setup | Done |
| 1.5 | Published paragraph (pilot) | Done |
| 2 | Notebook and interpreter modules | Planned |
The published paragraph was picked as pilot because it's read-only and has almost no coupling to other modules.
Angular host (port 4200) React remote (port 3001)
┌─────────────────────────┐ ┌─────────────────────────┐
│ paragraph.component.ts │ │ webpack.config.js │
│ loads remoteEntry.js ──┼──────>│ ModuleFederationPlugin │
│ calls mount(el, props) │ │ name: 'reactApp' │
└─────────────────────────┘ │ exposes: │
│ ./PublishedParagraph │
└─────────────────────────┘
remoteEntry.js from the React dev server or production assets.window.reactApp as a Module Federation container.container.get('./PublishedParagraph') to get the module.mount(element, props), which calls createRoot() and renders into the DOM element.Append ?react=true to any published paragraph URL to activate React mode.
Run npm install then npm run dev to start the dev server on http://localhost:3001.
The Angular host must be running on port 4200. From zeppelin-web-angular/, npm start runs both servers together.
From projects/zeppelin-react/, run npm run build. Output goes to dist/. In production, Angular loads remoteEntry.js from /assets/react/ (see environment.prod.ts).
From projects/zeppelin-react/, run npm run lint to check, npm run lint:fix to auto-fix. See eslint.config.js for rules.
src/ ├── components/ │ ├── common/ # Empty, Loading │ ├── renderers/ # HTMLRenderer, ImageRenderer, TextRenderer │ └── visualizations/ # TableVisualization, VisualizationControls ├── pages/ │ └── PublishedParagraph.tsx # entry component + mount() ├── templates/ │ └── SingleResultRenderer.tsx # routes result types to renderers ├── utils/ # tableUtils, textUtils, exportFile └── main.ts # re-exports for Module Federation
The Angular host loads each exposed module through the ReactMountDirective (see src/app/share/react-mount/). The contract:
export interface ReactMountHandle { update: (props: Props & { onError?: (e: unknown) => void }) => void; unmount: () => void; } export function mount(element: HTMLElement, props: Props): ReactMountHandle;
src/components/<area>/ExampleFeature.tsx).<ReactErrorBoundary onError={props.onError}>.mount(element, props) function that:Root via createRoot(element).root.render(<Wrapped {...props}/>) on initial mount AND on every update(newProps) call. React's reconciler preserves state.{ update, unmount }. unmount calls root.unmount().webpack.config.js under exposes:exposes: { './PublishedParagraph': './src/pages/PublishedParagraph', './ParagraphFooter': './src/components/paragraph/ParagraphFooter', './ExampleFeature': './src/components/<area>/ExampleFeature' }
main.ts:export { ExampleFeature, mount as mountExampleFeature } from './components/<area>/ExampleFeature';
<div zeppelin-react-mount="./ExampleFeature" [reactProps]="exampleFeatureProps" ></div>
exampleFeatureProps should be a getter on the host component (not an inline object literal) so identity is stable when nothing changed.The legacy ./PublishedParagraph module returns a bare unmount fn from mount. The directive tolerates that shape, but new modules should use the handle contract.