Home About Contact
Angular , React

Angular で React Component を使う(その2) Hello, World!

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 にブラウザでアクセスして作動を確かめます。

ReactComponent 用の ディレクティブを用意

./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()
    }
  }
}

React コンポーネントを用意

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 で MyReactComponentDirective を使う

最後に 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 {
}

ポイントは @Componenttemplate の記述です。 次のように記述しているので、結果として 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

結果:

MyReactComponentA

うまくいきました。

それではここで先ほど出ていた警告を解決します。

./angular.json に "allowedCommonJsDependencies": [ "react", "react-dom", "react-dom/client" ] を追加:

"architect": {
  "build": {
    "scripts": [],
    "allowedCommonJsDependencies": [ "react", "react-dom", "react-dom/client" ]
    ...

npm run build して警告が出なくなったことを確認しましょう。

Angular から React コンポーネントに値を渡す

Angular の template で自作の myReactComponent ディレクティブを使って Reactのコンポーネントを指定して表示させていました。

app.component.ts:

<div myReactComponent myKind="A"></div>',

次のようにタイトル値を Angular から React コンポーネントに渡すにはどうすればいいのか?

<div myReactComponent myKind="A" myTitle="Hello, World!"></div>',

MyParams

値(など)を受け渡しするためのインタフェースをつくります。

./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)

関数 MyReactComponentAMyParams を適用できるようにしています。

この例では params は title しか保持していないので params を使わないで、ディストラクチャリング(とかいう)仕組みを使って書くことこうなる。

const MyReactComponentA = ({ title }: MyParams) => {
  return (
    <div>{title}</div>
  )
}

これで JSX として自然な記述になる。

最後に MyReactComponentDirectiveMyReactComponentA に 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

React Component から Angular に通知

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 に ボタン要素のクリックを通知できました。