Hay dos escenarios que requieren el uso de React en una aplicación Angular. Primero, hay un componente en el ecosistema de React que nos llevará semanas desarrollar, por ejemplo, un componente Timeline.
En segundo lugar, puede que estemos trabajando una empresa que utiliza React y necesitemos para un nuevo requerimiento integrarlo en una aplicación existente que esté en angular.
En este artículo, mostraremos cómo integrar React en ambos casos. Empecemos por el caso más sencillo en el que debemos utilizar un componente React.
Renderizar un componente React
Vamos a crear una directiva que tome un componente React y sus props y lo renderice en el host. El artículo asume que estás familiarizado con React.
import { ComponentProps, createElement, ElementType } from 'react';
import { createRoot } from 'react-dom/client';
@Directive({
selector: '[reactComponent]',
standalone: true
})
export class ReactComponentDirective<Comp extends ElementType> {
@Input() reactComponent: Comp;
@Input() props: ComponentProps<Comp>;
private root = createRoot(inject(ElementRef).nativeElement)
ngOnChanges() {
this.root.render(createElement(this.reactComponent, this.props))
}
ngOnDestroy() {
this.root.unmount();
}
}
La directiva es sencilla. Toma un componente React y props, crea una raíz, y la rerenderiza cada vez que cambia.
Para nuestro ejemplo, renderizaremos el componente React Select dentro de un componente de página lazy todos. Lo instalaremos usando npm i react-select
y lo pasaremos a nuestra directiva
import Select from 'react-select';
import type { ComponentProps } from 'react';
@Component({
standalone: true,
imports: [CommonModule, ReactComponentDirective],
template: `
<h1>Todos page</h1>
<button (click)="changeProps()">Change</button>
<div [reactComponent]="Select" [props]="selectProps"></div>
`
})
export class TodosPageComponent {
Select = Select;
selectProps: ComponentProps<Select> = {
onChange(v) {
console.log(v)
},
options: [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
]
}
changeProps() {
this.selectProps = {
...this.selectProps,
options: [{ value: 'changed', label: 'Changed' }]
}
}
}
Ten en cuenta que el código React sólo se cargará cuando naveguemos por esta página porque nuestro componente .Todo se carga de forma perezosa.
Podemos ir un paso más allá y cargar los chunks completos de React bajo demanda. Ajustaremos la directiva de la siguiente manera:
import { Directive, ElementRef, Input } from '@angular/core';
import type { ComponentProps, ElementType } from 'react';
import type { Root } from 'react-dom/client';
@Directive({
selector: '[lazyReactComponent]',
standalone: true
})
export class LazyReactComponentDirective<Comp extends ElementType> {
@Input() lazyReactComponent: () => Promise<Comp>;
@Input() props: ComponentProps<Comp>;
private root: Root | null = null;
constructor(private host: ElementRef) { }
async ngOnChanges() {
const [{ createElement }, { createRoot }, Comp] = await Promise.all([
import('react'),
import('react-dom/client'),
this.lazyReactComponent()
]);
if (!this.root) {
this.root = createRoot(this.host.nativeElement);
}
this.root.render(createElement(Comp, this.props))
}
ngOnDestroy() {
this.root?.unmount();
}
}
Nuestro código ha sido modificado para utilizar la función import en lugar de eager imports
. Ahora podemos usarlo en nuestro componente de página todos:
import { CommonModule } from '@angular/common';
import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
import type { ComponentProps } from 'react';
@Component({
standalone: true,
imports: [CommonModule, LazyReactComponentDirective],
template: `
<h1>Todos page</h1>
<button (click)="showSelect = true">Show React Component</button>
<ng-container *ngIf="showSelect">
<button (click)="changeProps()">Change</button>
<div [lazyReactComponent]="Select" [props]="selectProps"></div>
</ng-container>
`
})
export class TodosPageComponent {
showSelect = false;
selectProps: ComponentProps<typeof import('react-select').default> = {
onChange(v) {
console.log(v)
},
options: [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
]
}
Select = () => import('react-select').then(m => m.default);
changeProps() {
this.selectProps = {
...this.selectProps,
options: [{ value: 'change', label: 'Change' }]
}
}
}
Renderizar una aplicación React
El proceso de renderizar una aplicación es casi idéntico al de renderizar un componente con un simple complemento corto.
Nuestro objetivo es exponer el injector
de aplicaciones de Angular a la aplicación React renderizada para poder utilizar los servicios de Angular dentro de ella.
Para ello, usaremos React Context, que expone el injector
y renderiza la aplicación React dentro de él:
import { Injector } from '@angular/core';
import { PropsWithChildren, createContext, useContext } from 'react';
import { createRoot, Root } from 'react-dom/client'
const InjectorCtx = createContext<Injector | null>(null)
export function NgContext(props: PropsWithChildren<{ injector: Injector }>) {
return createElement(InjectorCtx.Provider, {
children: props.children,
value: props.injector
})
}
function useInjector(): Injector {
const injector = useContext(InjectorCtx);
if (!injector) {
throw new Error('Missing NgContext')
}
return injector;
}
Hemos creado un Context
que proporciona un inyector Angular y un hook useInjector
. A continuación, implementaremos un servicio que renderiza un componente React:
// ... THE CONTEXT CODE IS ABOVE ...
@Injectable({ providedIn: 'root' })
export class NgReact {
injector = inject(Injector);
createRoot(host: HTMLElement) {
return createRoot(host);
}
render<Comp extends ElementType>(
root: Root,
Comp: Comp,
compProps?: ComponentProps<Comp>
) {
root.render(
createElement(NgContext, {
injector: this.injector,
}, createElement(Comp, compProps))
)
}
}
El método render()
renderiza el componente React proporcionado bajo el proveedor NgContext
para que pueda acceder al inyector Angular que proporcionamos.
He creado una aplicación React con Nx que utiliza nuestra función useInjector:
import NxWelcome from './nx-welcome';
export function App() {
return (
<>
<NxWelcome title="react-platform" />
...
</>
);
}
import { Router } from '@angular/router';
import { useInjector } from '@myorg/ng-react';
export function NxWelcome({ title }: { title: string }) {
const injector = useInjector();
return <>
....
<button onClick={() => injector.get(Router).navigateByUrl('/')}>Home</button>
...
</>
}
Navegamos a nuestra página de inicio cada vez que hacemos clic en el botón de inicio utilizando el router
Angular que obtenemos del injector
Vamos a renderizarlo en nuestro componente de página todos:
import { Component, ElementRef, inject } from '@angular/core';
import { App } from '@myorg/app-name';
import { NgReact } from '@myorg/ng-react';
@Component({
standalone: true,
template: ``
})
export class TodosPageComponent {
private ngReact = inject(NgReact);
private root = this.ngReact.createRoot(inject(ElementRef).nativeElement);
ngOnInit() {
this.ngReact.render(this.root, App)
}
ngOnDestroy() {
this.root.unmount();
}
}
view raw
Conclusión:
Es importante que siempre se pueda tener la posibilidad de integrar y aprovechar los recursos que nos ofrecen las diversas librerías o frameworks, en este ejemplo pudimos observar que tanto react como angular se convierten en ese medio para lograr un fin, son herramientas, son muy diferentes y por ende sería un poco absurdo compararlos entre si, ya que react es una librería, angular es un framework no es lo mismo, son cosas diferentes que están diseñadas para entornos diferentes.