Skip to content

Commit d4851d4

Browse files
authored
feat: add MF React and Preact example (#4323)
1 parent 8ec14d7 commit d4851d4

18 files changed

+6417
-5073
lines changed

pnpm-lock.yaml

Lines changed: 6151 additions & 5073 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# React and Preact Integration at Runtime
2+
3+
This example demonstrates how to run a React-based application (shell) while consuming a remote Preact-based application (remote) dynamically at runtime.
4+
5+
- `shell`: is the host application using React and ReactDOM.
6+
- `remote`: The guest application built with Preact. It provides an injector function that allows the host application (shell) to import and mount it into a specified `<div>` element.
7+
8+
# How to Run the Demo
9+
10+
Run `pnpm run start`. This will build and serve both `shell` and `remote` on ports 3001 and 3002 respectively.
11+
12+
- [localhost:3001](http://localhost:3001/) (HOST)
13+
- [localhost:3002](http://localhost:3002/) (STANDALONE REMOTE)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "react-preact-runtime-typescript",
3+
"description": "This example demos host and remote applications running in isolation with React and Preact",
4+
"version": "0.0.0",
5+
"scripts": {
6+
"start": "pnpm --filter react-preact-runtime-typescript-* --parallel start",
7+
"build": "pnpm --filter react-preact-runtime-typescript-* --parallel build"
8+
}
9+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "react-preact-runtime-typescript-remote",
3+
"private": true,
4+
"version": "1.0.0",
5+
"scripts": {
6+
"start": "rsbuild dev",
7+
"build": "rsbuild build",
8+
"preview": "rsbuild preview"
9+
},
10+
"dependencies": {
11+
"preact": "^10.25.1"
12+
},
13+
"devDependencies": {
14+
"@module-federation/rsbuild-plugin": "^0.8.4",
15+
"@rsbuild/core": "^1.1.8",
16+
"@rsbuild/plugin-preact": "^1.2.0",
17+
"typescript": "^5.7.2"
18+
}
19+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
2+
import { defineConfig } from '@rsbuild/core';
3+
import { pluginPreact } from '@rsbuild/plugin-preact';
4+
5+
export default defineConfig({
6+
plugins: [
7+
pluginPreact(),
8+
pluginModuleFederation({
9+
name: 'remote',
10+
exposes: {
11+
'./appInjector': './src/appInjector',
12+
},
13+
}),
14+
],
15+
server: {
16+
port: 3002,
17+
},
18+
tools: {
19+
rspack: {
20+
output: {
21+
uniqueName: 'remote',
22+
publicPath: 'auto',
23+
},
24+
},
25+
},
26+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import LocalButton from './Button';
2+
3+
const App = () => (
4+
<div style={{ border: '1px red solid' }}>
5+
<h1>Remote Application - Preact</h1>
6+
<LocalButton />
7+
</div>
8+
);
9+
10+
export default App;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const Button = () => <button>Remote Button</button>;
2+
3+
export default Button;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import App from './App';
2+
import { render } from 'preact';
3+
4+
let root: HTMLElement | null = null;
5+
6+
export const inject = (parentElementId: string) => {
7+
const parentElement = document.getElementById(parentElementId);
8+
if (!parentElement) {
9+
console.error(`Element with id '${parentElementId}' not found.`);
10+
return;
11+
}
12+
13+
root = parentElement;
14+
render(<App />, root);
15+
};
16+
17+
export const unmount = () => {
18+
if (root) {
19+
render(null, root);
20+
root = null;
21+
} else {
22+
console.warn(
23+
'Root is not defined. Ensure inject() is called before unmount().',
24+
);
25+
}
26+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { render } from 'preact';
2+
import App from './App';
3+
4+
const root = document.getElementById('root');
5+
if (root) {
6+
render(<App />, root);
7+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import('./bootstrap');
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["DOM", "ES2020"],
4+
"jsx": "react-jsx",
5+
"target": "ES2020",
6+
"noEmit": true,
7+
"skipLibCheck": true,
8+
"jsxImportSource": "preact",
9+
"useDefineForClassFields": true,
10+
11+
/* modules */
12+
"module": "ESNext",
13+
"isolatedModules": true,
14+
"resolveJsonModule": true,
15+
"moduleResolution": "Bundler",
16+
"allowImportingTsExtensions": true,
17+
"paths": {
18+
"react": ["./node_modules/preact/compat/"],
19+
"react-dom": ["./node_modules/preact/compat/"]
20+
},
21+
22+
/* type checking */
23+
"strict": true,
24+
"noUnusedLocals": true,
25+
"noUnusedParameters": true
26+
},
27+
"include": ["src"]
28+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "react-preact-runtime-typescript-shell",
3+
"private": true,
4+
"version": "1.0.0",
5+
"scripts": {
6+
"start": "rsbuild dev",
7+
"build": "rsbuild build",
8+
"preview": "rsbuild preview"
9+
},
10+
"dependencies": {
11+
"react": "^19.0.0",
12+
"react-dom": "^19.0.0"
13+
},
14+
"devDependencies": {
15+
"@module-federation/enhanced": "^0.8.4",
16+
"@rsbuild/core": "^1.1.8",
17+
"@rsbuild/plugin-react": "^1.0.7",
18+
"@types/react": "^19.0.0",
19+
"@types/react-dom": "^19.0.0",
20+
"typescript": "^5.7.2"
21+
}
22+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig } from '@rsbuild/core';
2+
import { pluginReact } from '@rsbuild/plugin-react';
3+
4+
export default defineConfig({
5+
plugins: [pluginReact()],
6+
server: {
7+
port: 3001,
8+
},
9+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useEffect } from 'react';
2+
import { init, loadRemote } from '@module-federation/enhanced/runtime';
3+
4+
interface RemoteModule {
5+
inject: (parentElementId: string) => void;
6+
unmount: () => void;
7+
}
8+
9+
init({
10+
name: 'shell',
11+
remotes: [
12+
{
13+
name: 'remote',
14+
entry: 'http://localhost:3002/mf-manifest.json',
15+
},
16+
],
17+
});
18+
19+
const parentElementId = 'parent';
20+
21+
const App = () => {
22+
useEffect(() => {
23+
let unmountRemote: () => void;
24+
25+
const loadRemoteApp = async () => {
26+
try {
27+
const module = await loadRemote<RemoteModule>('remote/appInjector');
28+
if (!module) return;
29+
const { inject, unmount } = module;
30+
unmountRemote = unmount;
31+
32+
inject(parentElementId);
33+
} catch (error) {
34+
console.error('Error loading the Remote:', error);
35+
}
36+
};
37+
38+
loadRemoteApp();
39+
40+
return () => {
41+
if (unmountRemote) unmountRemote();
42+
};
43+
}, []);
44+
45+
// Remote will be injected in the div with parentElementId
46+
return (
47+
<div>
48+
<h1>Host Application - React</h1>
49+
<h2>Remote</h2>
50+
<div id={parentElementId} />
51+
</div>
52+
);
53+
};
54+
55+
export default App;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom/client';
3+
import App from './App';
4+
5+
const rootEl = document.getElementById('root');
6+
if (rootEl) {
7+
const root = ReactDOM.createRoot(rootEl);
8+
root.render(
9+
<React.StrictMode>
10+
<App />
11+
</React.StrictMode>,
12+
);
13+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="@rsbuild/core/types" />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import('./bootstrap');
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["DOM", "ES2020"],
4+
"jsx": "react-jsx",
5+
"target": "ES2020",
6+
"noEmit": true,
7+
"skipLibCheck": true,
8+
"useDefineForClassFields": true,
9+
10+
/* modules */
11+
"module": "ESNext",
12+
"isolatedModules": true,
13+
"resolveJsonModule": true,
14+
"moduleResolution": "Bundler",
15+
"allowImportingTsExtensions": true,
16+
17+
/* type checking */
18+
"strict": true,
19+
"noUnusedLocals": true,
20+
"noUnusedParameters": true
21+
},
22+
"include": ["src"]
23+
}

0 commit comments

Comments
 (0)