Skip to content

Commit cd4b202

Browse files
feat: add different-react-versions-typescript (#2745)
Co-authored-by: Bruno Silva <bruno3dcontato@gmail.com>
1 parent 0a8f3e6 commit cd4b202

30 files changed

+8757
-4
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Mixed React Versions and Compatibility levels
2+
3+
This example demos the ability to load two separate versions of react (v16.6.3 and v18.2.0).
4+
5+
> Check the javascript version of this example [here](../different-react-versions/README.md).
6+
7+
Module Federation allows us to create an adapter which attaches a hooks-friendly version to render a section of thr app using modern versions.
8+
9+
- `app1` uses and older version of react, not compatible with react Hooks
10+
- `app2` uses a modern react version and its components are hooks based
11+
12+
## Running Demo
13+
14+
Run `yarn start`. This will build and serve both `app1` and `app2` on ports 3001 and 3002 respectively.
15+
16+
- [localhost:3001](http://localhost:3001/) (HOST)
17+
- [localhost:3002](http://localhost:3002/) (STANDALONE REMOTE)
18+
19+
## How it works
20+
21+
This example contains two important components, the `ReactAdapterConsumer` and `ReactAdapterProvider`. They are responsible to make the two versions of react work together.
22+
23+
The adapter consumes both versions of react to "translate" the props into a fresh render. This could be presented as a HOC or federated components could have a legacy export containing the adapter build in.
24+
25+
### [ReactAdapterProvider](./app2/src/components/ReactAdaperProvider.tsx)
26+
27+
This component is responsible to dynamic render/hydrate the federated component using it host version of React.
28+
29+
> You can see the usage [here](./app2/src/components/ModernReactComponent.tsx#29).
30+
31+
This is a generic component type, so you can pass the generic parameter to the component to specify the type of the props.
32+
33+
```jsx
34+
import React from 'react';
35+
36+
export interface ButtonProps {
37+
color: 'red' | 'blue';
38+
}
39+
40+
const Button = (props: ButtonProps) => {
41+
return <button style={{ color: props.color }}>Click me</button>;
42+
};
43+
44+
export const Adapted = React.forwardRef<
45+
ReactAdaperProvider<ModernReactComponentProps>,
46+
ModernReactComponentProps
47+
>((props, ref) => {
48+
// the intellisesne will show the type of the props if you try to modify it
49+
return (
50+
<ReactAdaperProvider<ButtonProps> component={Button} color="red" ref={ref} />
51+
);
52+
});
53+
```
54+
55+
### [ReactAdapterConsumer](./app1/src/components/ReactAdapterConsumer.tsx)
56+
57+
This component is responsible to render the federated component using the remote version of React.
58+
59+
> You can see the usage [here](./app1/src/components/App.tsx#41).
60+
61+
This is a generic component type, so you can pass the generic parameter to the component to specify the type of the props.
62+
63+
```jsx
64+
// remeber to add path alias to your tsconfig.base.json at the root of the workspace and the type definition file of the remote component
65+
// this demo contains an example that reproduce that but you can check in the gist below
66+
// https://gist.github.com/brunos3d/80235047c74b27573234c774ed474ef8
67+
import type { ButtonProps } from 'app2/Button';
68+
69+
<ReactAdapterConsumer<ButtonProps>
70+
// you can try to modify the color value and the intellisense automatically will show the type of the props
71+
color="blue"
72+
fallback={<div>Loading...</div>}
73+
importer={() => import('app2/Button').then(module => ({ default: module.Adapted }))}
74+
/>;
75+
```
76+
77+
<img src="https://ssl.google-analytics.com/collect?v=1&t=event&ec=email&ea=open&t=event&tid=UA-120967034-1&z=1589682154&cid=ae045149-9d17-0367-bbb0-11c41d92b411&dt=ModuleFederationExamples&dp=/email/DifferentReactVersionsTypescript">
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"plugins": [
3+
"@babel/plugin-proposal-class-properties"
4+
],
5+
"presets": [
6+
"@babel/preset-react",
7+
"@babel/preset-typescript"
8+
]
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
declare module 'app2/Button' {
2+
export * from 'app2/src/components/Button';
3+
}
4+
5+
declare module 'app2/ModernComponent' {
6+
export * from 'app2/src/components/ModernReactComponent';
7+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@different-react-versions-typescript/app1",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"start": "webpack-cli serve",
7+
"build": "webpack --mode production",
8+
"serve": "serve dist -p 3001",
9+
"clean": "rm -rf dist"
10+
},
11+
"dependencies": {
12+
"react": "16.6.3",
13+
"react-dom": "16.14.0"
14+
},
15+
"devDependencies": {
16+
"@babel/core": "7.18.9",
17+
"@babel/plugin-proposal-class-properties": "7.18.6",
18+
"@babel/preset-react": "7.18.6",
19+
"@babel/preset-typescript": "^7.18.6",
20+
"@types/react": "16.9.56",
21+
"@types/react-dom": "16.9.17",
22+
"babel-loader": "8.2.5",
23+
"html-webpack-plugin": "5.5.0",
24+
"serve": "13.0.4",
25+
"typescript": "~4.8.4",
26+
"webpack": "5.72.1",
27+
"webpack-cli": "4.9.2",
28+
"webpack-dev-server": "4.9.3"
29+
}
30+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<html>
2+
<head>
3+
<script src="<%= htmlWebpackPlugin.options.app2RemoteEntry %>"></script>
4+
</head>
5+
<body>
6+
<div id="root"></div>
7+
</body>
8+
</html>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import App from './components/App';
4+
ReactDOM.render(<App />, document.getElementById('root'));
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from 'react';
2+
import ReactAdapterConsumer from './ReactAdapterConsumer';
3+
4+
// you can import types with alias
5+
import type { ButtonProps } from 'app2/Button';
6+
7+
const RemoteButton = React.lazy(
8+
() => import('app2/Button') as Promise<{ default: React.ComponentType<ButtonProps> }>,
9+
);
10+
11+
// const ModernComponent = React.lazy(() => import("app2/ModernComponent"));
12+
// Hooks not suppoorted, uncomment to verify this is a pre-hooks react version being used.
13+
// import HookComponent from './ComponentWithHook'
14+
15+
export interface AppProps {}
16+
17+
export interface AppState {
18+
input: string;
19+
}
20+
21+
class App extends React.Component<AppProps, AppState> {
22+
constructor(props: AppProps) {
23+
super(props);
24+
this.state = { input: '' };
25+
this.setValue = this.setValue.bind(this);
26+
}
27+
28+
setValue(e: React.ChangeEvent<HTMLInputElement>) {
29+
this.setState({ input: e.target.value });
30+
}
31+
32+
render() {
33+
return (
34+
<div>
35+
<h1>Basic Host-Remote</h1>
36+
<h2>App 1, Uses react version not compatible with hooks</h2>
37+
<input onChange={this.setValue} placeholder="Type something into this input" />
38+
39+
<div style={{ border: '1px red solid', padding: '10px', margin: '20px 0' }}>
40+
{/* the generic component accepts property types */}
41+
<ReactAdapterConsumer<AppState>
42+
// any other props, passed to ModernComponent
43+
{...this.state}
44+
fallback={<div>Loading...</div>}
45+
importer={() =>
46+
import('app2/ModernComponent').then(module => ({ default: module.Adapted }))
47+
}
48+
>
49+
<h3>And these are children passed into it from the legacy app</h3>
50+
</ReactAdapterConsumer>
51+
</div>
52+
53+
{/*This will Fail*/}
54+
{/*<HookComponent/>*/}
55+
56+
<div style={{ border: '1px red solid', padding: '10px', margin: '20px 0' }}>
57+
<React.Suspense fallback="Loading Button">
58+
{/* The autocomplete works here, try it modifing the color value */}
59+
<RemoteButton color="red" />
60+
</React.Suspense>
61+
</div>
62+
63+
{/*This will fail without Adapter*/}
64+
{/*<React.Suspense fallback="Loading Button">*/}
65+
{/* <ModernComponent />*/}
66+
{/*</React.Suspense>*/}
67+
</div>
68+
);
69+
}
70+
}
71+
72+
export default App;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React, { useEffect } from 'react';
2+
3+
const ComponentWithHook = () => {
4+
React.useEffect(() => {
5+
console.log('some effect from app1');
6+
}, []);
7+
8+
return <span>This should break, no hooks supported in this app.</span>;
9+
};
10+
11+
export default ComponentWithHook;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
3+
export type ReactAdapterConsumerProps<P = {}> = Partial<P> & {
4+
fallback: React.SuspenseProps['fallback'];
5+
// the Adapter will be the same as the exported from ReactAdapterProvider
6+
importer: () => Promise<{
7+
default: React.ComponentType<P> | React.ExoticComponent<P> | React.FunctionComponent<P>;
8+
}>;
9+
};
10+
11+
export interface ReactAdapterConsumerState {
12+
Component: React.ReactNode;
13+
}
14+
15+
class ReactAdapterConsumer<P = {}> extends React.Component<
16+
ReactAdapterConsumerProps<P>,
17+
ReactAdapterConsumerState
18+
> {
19+
private RemoteComponent: React.LazyExoticComponent<React.ComponentType<P>>;
20+
21+
constructor(props: ReactAdapterConsumerProps<P>) {
22+
super(props);
23+
this.state = { Component: () => null };
24+
this.RemoteComponent = React.lazy(() => this.props.importer());
25+
}
26+
27+
render() {
28+
return (
29+
<React.Suspense fallback={this.props.fallback}>
30+
<this.RemoteComponent {...(this.props as P)} />
31+
</React.Suspense>
32+
);
33+
}
34+
}
35+
36+
export default ReactAdapterConsumer;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import('./bootstrap');

0 commit comments

Comments
 (0)