React в приложениях Angular может понадобится в двух случаях.
- В экосистеме React есть компонент, на разработку которого, вероятно, уйдут недели, например компонент Timeline.
- Сотрудничество с компанией, использующей React, которой необходимо интегрировать его в существующее приложение.
В этой статье я расскажу, как интегрировать React в обоих сценариях. Начнем с самого простого случая, когда нужно использовать компонент React.
Рендеринг компонента React
Создадим директиву, которая принимает компонент React и пропсы и осуществляет рендеринг на хост. Предположу, что вы уже знакомы с 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();
}
}
Директива берет компонент React и пропсы, создает корень и совершает повторный рендеринг при каждом его изменении.
В этом примере мы будем рендерить компонент React Select внутри ленивого компонента страницы todos
. Мы установим его с помощью npm i react-select
и передадим директиве:
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' }]
}
}
}
Обратите внимание: код React будет загружаться только при навигации по этой странице, потому что компонент todo
загружается отложено.
Мы можем пойти дальше и загружать целые куски React по мере необходимости. Настроим директиву следующим образом:
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();
}
}
Код был изменен для использования функции import
вместо eager imports
. Теперь мы можем задействовать ее в компоненте страницы 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' }]
}
}
}
Рендеринг приложения React
Процесс рендеринга приложения практически идентичен рендерингу компонента с одним небольшим отличием. Наша цель — открыть injector
приложения Angular отрендеренному приложению React, чтобы можно было использовать в нем сервисы Angular. Для этого пригодится React Context, который открывает injector
и рендерит React-приложение внутри него:
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;
}
Мы создали Context
, предоставляющий инжектор Angular и хук useInjector
. Далее реализуем сервис, который рендерит 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))
)
}
}
Метод render()
рендерит предоставленный React-компонент с помощью провайдера NgContext
, чтобы он мог получить доступ к предоставленному injector
Angular.
Я создал React-приложение с помощью инструмента Nx, который использует функцию 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>
...
</>
}
Мы переходим на домашнюю страницу при каждой нажатии на кнопку home
, используя router
Angular, который получаем из injector
.
Отрендерим его в компоненте страницы 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();
}
}
Читайте также:
- Анализ работы Guess.js в приложении Angular
- 7 самых популярных библиотек React
- Осторожно! Angular крадет ваше время
Читайте нас в Telegram, VK и Дзен
Перевод статьи Netanel Basal: Using React in Angular Applications