Angular + React の覚え書きです。 今回は directive 経由で実装します。
$ node --version
v18.20.5
$ npm --version
10.8.2
ng コマンドをグローバルに入れたくないので次の手順で Angular v19 のプロジェクトのベースをつくります。
tmp ディレクトリを作成して、ng コマンドをいれます。
$ mkdir tmp
$ cd tmp
$ npm install @angular/cli
@angular/cli で現時点では Angular の v19.2.0 が入る。
その後、hello-world プロジェクトを作成:
$ npx ng new hello-world --strict --skip-install
適当に質問にこたえると ./hello-world にプロジェクトのベースができている。 この ./hello-world 内で Angular アプリをつくっていく。
$ cd hello-world
$ npm install
$ npm start
http://localhost:4200 にブラウザでアクセスして作動を確かめます。
./src/app/my-react-component.directive.ts:
import {Directive, ElementRef, Input, OnInit, OnDestroy} from '@angular/core'
import React from 'react'
import ReactDOM from 'react-dom/client'
import MyReactComponentA from './react-components/MyReactComponentA'
import MyReactComponentB from './react-components/MyReactComponentB'
@Directive({
standalone: true,
selector: '[myReactComponent]'
})
export class MyReactComponentDirective implements OnInit, OnDestroy {
@Input('myKind') kind = 'A'
private root: ReactDOM.Root | undefined
constructor(private el: ElementRef){
}
ngOnInit() {
this.root = ReactDOM.createRoot(this.el.nativeElement)
switch( this.kind ){
case 'A':
this.root.render(React.createElement(MyReactComponentA))
break
case 'B':
this.root.render(React.createElement(MyReactComponentB))
break
default :
this.root.render(React.createElement(MyReactComponentA))
break
}
}
ngOnDestroy(): void {
if( this.root ){
this.root.unmount()
}
}
}
MyReactComponentDirective (my-react-component.directive.ts) で使用した MyReactComponentA, MyReactComponentB を用意します。
./src/app/react-components/MyReactComponentA.tsx:
import React from 'react'
const MyReactComponentA = () => {
return (
<div>ReactComponent <b>A</b></div>
)
}
export default MyReactComponentA
./src/app/react-components/MyReactComponentB.tsx:
import React from 'react'
const MyReactComponentB = () => {
return (
<div>ReactComponent <b>B</b></div>
)
}
export default MyReactComponentB
最後に app.component.ts を用意したディレクティブを使ってかきかえます。
./src/app/app.component.ts:
import { Component } from '@angular/core'
import { MyReactComponentDirective } from './my-react-component.directive'
@Component({
standalone: true,
selector: 'app-root',
imports: [MyReactComponentDirective],
template: '<div myReactComponent myKind="A"></div>',
})
export class AppComponent {
}
ポイントは @Component の template の記述です。 次のように記述しているので、結果として MyReactComponentA が表示されます。
<div myReactComponent myKind="A"></div>
A の代わりに B を指定すれば MyReactComponentB が表示されます。
<div myReactComponent myKind="B"></div>
JSX内に直接 A または B を指定するのではなく myKind を変数を経由して指定することもできます。
import { Component } from '@angular/core' import { MyReactComponentDirective } from './my-react-component.directive' @Component({ standalone: true, selector: 'app-root', imports: [MyReactComponentDirective], template: '<div myReactComponent [myKind]="myKind"></div>', }) export class AppComponent { myKind = 'A' //myKind = 'B' }
Angular のプロジェクトで React を使用しているので、それらに必要なモジュールをインストールします。
npm install react react-dom
npm install @types/react @types/react-dom --save-dev
それではビルドしみましょう。 プロジェクトのルートで:
npm run build
はい、エラーになります。順番に問題を解決していきます。
ERROR] TS6142: Module './react-components/MyReactComponentA' was resolved to ...
src/app/my-react-component.directive.ts:4:30:
4 │ import MyReactComponentA from './react-components/MyReactComponentA'
╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
./tsconfig.json の compilerOptions に次を追加します。
"compilerOptions": {
"jsx": "react",
...
再度 npm run build すると 警告:
[WARNING] Module 'react' used by 'src/app/my-react-component.directive.ts' is not ESM
が出ます。 ビルド自体は成功しまたので、先に進みます。(あとでこの警告は解決します。)
ここで npm start して意図通り MyReactComponentA が表示できるか確認します。
npm start
結果:
うまくいきました。
それではここで先ほど出ていた警告を解決します。
./angular.json に "allowedCommonJsDependencies": [ "react", "react-dom", "react-dom/client" ] を追加:
"architect": {
"build": {
"scripts": [],
"allowedCommonJsDependencies": [ "react", "react-dom", "react-dom/client" ]
...
npm run build して警告が出なくなったことを確認しましょう。
Angular の template で自作の myReactComponent ディレクティブを使って Reactのコンポーネントを指定して表示させていました。
app.component.ts:
<div myReactComponent myKind="A"></div>',
次のようにタイトル値を Angular から React コンポーネントに渡すにはどうすればいいのか?
<div myReactComponent myKind="A" myTitle="Hello, World!"></div>',
値(など)を受け渡しするためのインタフェースをつくります。
./src/app/MyParams.ts:
export interface MyParams {
title: string;
}
MyReactComponentA がこの MyParams を受け取ってその値を表示できるように変更します。
import React from 'react'
import { MyParams } from '../MyParams'
const MyReactComponentA = (params: MyParams) => {
return (
<div>{params.title}</div>
)
}
export default MyReactComponentA
ポイントはここ:
const MyReactComponentA = (params: MyParams)
関数 MyReactComponentA に MyParams を適用できるようにしています。
この例では params は title しか保持していないので params を使わないで、ディストラクチャリング(とかいう)仕組みを使って書くことこうなる。
const MyReactComponentA = ({ title }: MyParams) => { return ( <div>{title}</div> ) }
これで JSX として自然な記述になる。
最後に MyReactComponentDirective で MyReactComponentA に MyParams のインスタンスを適用するように修正します。
./src/app/my-react-component.directive.ts:
import { MyParams } from './MyParams'
...
export class MyReactComponentDirective implements OnInit, OnDestroy {
...
@Input('myTitle') title = ''
...
ngOnInit() {
...
const params: MyParams = { title: this.title }
switch( this.kind ){
case 'A':
//this.root.render(React.createElement(MyReactComponentA))
this.root.render(React.createElement(MyReactComponentA, params))
...
my-react-component.directive.ts のコード全体は次のようになりました。
import {Directive, ElementRef, Input, OnInit, OnDestroy} from '@angular/core' import React from 'react' import ReactDOM from 'react-dom/client' import MyReactComponentA from './react-components/MyReactComponentA' import MyReactComponentB from './react-components/MyReactComponentB' import { MyParams } from './MyParams' @Directive({ standalone: true, selector: '[myReactComponent]' }) export class MyReactComponentDirective implements OnInit, OnDestroy { @Input('myKind') kind = 'A' @Input('myTitle') title = '' private root: ReactDOM.Root | undefined constructor(private el: ElementRef){ } ngOnInit() { this.root = ReactDOM.createRoot(this.el.nativeElement) const params: MyParams = { title: this.title } switch( this.kind ){ case 'A': //this.root.render(React.createElement(MyReactComponentA)) this.root.render(React.createElement(MyReactComponentA, params)) break case 'B': this.root.render(React.createElement(MyReactComponentB)) break default : //this.root.render(React.createElement(MyReactComponentA)) this.root.render(React.createElement(MyReactComponentA, params)) break } } ngOnDestroy(): void { if( this.root ){ this.root.unmount() } } }
最後に app.component.ts の template で myTitle 属性を設定します。
template: '<div myReactComponent myKind="A" myTitle="Hello, World!"></div>',
それでは npm start して意図通り反映できたか確認します。
結果:
MyReactComponentA で次のようにボタン要素を追加。それをクリックしたら params.buttonClicked() を呼ぶようにします。
const MyReactComponentA = (params: MyParams) => {
...
return (
...
<button type="button" onClick={ ()=>params.buttonClicked() }>Click</button>
MyParams に buttonClicked を追加:
export interface MyParams {
...
buttonClicked: ()=>void
Angular 側の MyReactComponentDirective で buttonClicked のコールバックを受け取る:
export class MyReactComponentDirective implements OnInit, OnDestroy {
...
ngOnInit() {
...
const myParams: MyParams = {
title: this.title,
buttonClicked: ():void => {
console.log('Button was Clicked!')
}
}
これで React Component から Angular に ボタン要素のクリックを通知できました。