Glue Angular and React with Web Components
Demo: hybird-angular-react-custom-element
There’s a lot of discussion about all of those front-end libraries/frameworks. I noticed that there is a common argument saying “React is just a view library” and “Angular is a full-function framework”.
Angular is considered a framework because it offers strong opinions as to how your application should be structured. It also has much more functionality “out-of-the-box”. You don’t need to decide which routing libraries to use or other such considerations – you can just start coding.
– React vs. Angular: The Complete Comparison
They’re referenceable. It raises an interesting question regarding whether we use React just as a view library to handle queries regarding UI, and use Angular to structure all others?
The zeroth step
Create a new empty project with Angular CLI, then install React.
Let’s start with an example of a counter.
The first step: A static UI
The very first step is to transform React Component into Custom Element then render the custom element in the template of Angular. This step is easy to do.
1 | export function ReactToCustomElement<T>(ReactComponent: React.ComponentType<T> & CustomElementOptions) { |
After registered the React component to the custom element registry, use it in the angular template.
Result:
1 | <!-- Angular host --> |
Props from Angular to React
Next step, let’s try to feed props from the Angular template to React Component
1 | <!-- Angular template --> |
🔽
1 | // JSX |
By writing the template above, Angular will set the props as a property on the custom element. Now, it’s time to research how to collect the properties and transform to React props?
attributeChangedCallback?
attributeChangedCallback is one of the life cycle callbacks of the custom element.
Unfortunately, this method needs the props to be watched hard coded (by static get observedAttributes
). It can be used with ReactComponent.propTypes
.
Dirty check?
Check all props every animation frame.
It works in a ugly way.
Proxy
By replacing the prototype of the custom element to a Proxy, any attribute sets on the element can be caught by the proxy.
1 | class CustomElement extends HTMLElement { |
Problem: React will also set some properties on the root element (like __reactInternalInstance$sgc4z2mg06o
) in this manner a loop will occur.
Solution: Mount React in a child element.
1 | <!-- Angular host --> |
Event listener from Angular to React
Next step, translate the React onEvent
pattern to HTML’s addEventListener
pattern.
On Angular 7.2
Angular will call element.addEventListener
. Overwrite the addEventListener
method when creating custom element is enough.
On Angular 8.2
Angular will call EventTarget.prototype.addEventListener
instead of our overwritten version.
Possible solutions:
Provide Proxy as props to React.
Found it is impossible. React.createElement
will make a copy of the props with all properties on the props object.
Hack addEventListener
Another problem: Angular is not going to access the EventTarget.prototype.addEventListener
in the runtime. Angular is keeping the reference since the app is initialized.
Solution: Run the hack code before Angular (write it in src/index.html
)
1 | EventTarget.prototype.addEventListener = new Proxy(EventTarget.prototype.addEventListener, { |
Now, Angular will use the overwritten version of addEventListener
, it is possible to transform listeners from Angular to React props.
1 | addEventListener(event: string, handler, options) { |
Until now, Angular and React can communicate with property={p}
<==> [property]="p"
and onChange={f}
<==> (onChange)="onChange($event)"
.
Type level enforcement
A major advantage of Angular is it is using TypeScript with a custom compiler to integrate the template with TypeScript.
To use custom elements in Angular, a CUSTOM_ELEMENTS_SCHEMA must be set in the module. By using this schema, type checking in the template is disabled (if I recall it correctly). It is also not possible to “define” the type of custom element in Angular.
Enforce type of Angular component from the type of the React component
1 | - export class CounterComponent implements OnInit { |
Done. If the props of Counter
changed, TypeScript will complain Class 'CounterComponent' incorrectly implements interface 'ReactComponentProps<typeof Counter>'.
.
Generate Angular template from React props
Since it is impossible to define the type of a custom element in the Angular template, generate the template for the Angular component is a way to resolve this problem.
In the last step, the Angular component is enforced to implement React props, there is some information available on the class in runtime.
For methods, it will appear on the prototype of the component class. For class fields, TypeScript will transform it like the following:
1 | class I { |
A hacky way is to call .toString()
and scan all this.*
to collect all properties.
Now, this solution is fully typed.
Upgrade to Angular 9
This solution needs dynamically generate the Angular in runtime so it is impossible to be statically analyzed.
Disable the AOT and Ivy compiler.
A Todo MVC
To prove this solution is working, I write a Todo MVC demo.
Problems:
Changes on a mutable object can not invoke a re-render
Solution: Invoke a re-render after an event in the addEventListener
Props modifications in async operations can not invoke a re-render
Angular itself is using Zone.js to schedule a check after the async task is complete. It is also a good approach to schedule a check.
Solution: hack Zone.current._zoneDelegate.__proto__.invokeTask
to get angular zone then add a callback on all async task has done.
1 | const onAngularZoneCallbackMap = new Set<(...args: any[]) => void>(); |
1 | // in the custom element's constructor |
Glue Angular and React with Web Components
https://blog.jackworks.dev/2019/Glue-Angular-and-React-with-Web-Component/