Home About Contact
Angular , React

Angular から React への移行【その2】

Overview

Angular から React への移行【その1】 で作成した Angular アプリを部分的に React にします。

事前準備

これ Angular で React Component を使う(その1) Hello, World! 参考にしつつ、まず React 関連のモジュールを入れる:

$ npm install react react-dom
$ npm install @types/react @types/react-dom --save-dev

さらに React 関連のソースをビルドできるように tsconfig.json, angular.json を修正します。

tsconfig.json に jsx を追記:

{
  "compileOnSave": false,
  "compilerOptions": {
    "jsx": "react",
...

angular.json に allowedCommonJsDependencies を追記:

    "build": {
      "builder": "@angular/build:application",
      "options": {
        "allowedCommonJsDependencies": [
          "react", "react-dom", "react-dom/client"
        ],
    ...

About コンポーネントを React にする

About はポケモン情報を表示するだけの単純なコンポーネントなので、 これをはじめに React にします。

src/app/about/AboutComponentProps.ts を作成:

import { Pokemon } from '../model/pokemon';

export interface AboutComponentProps {
  pokemon: Pokemon | undefined;
}

これは about.ts から これから作成する React コンポーネント AboutComponent へ受け渡すパラメータを定義したインタフェースです。

src/app/about/AboutComponent.tsx を作成:

import React from 'react';
import { AboutComponentProps } from './AboutComponentProps';

export const AboutComponent = (props: AboutComponentProps): React.ReactElement => {
  return (
    <div>
      {props.pokemon?.name ?? 'unknown'}
    </div>
  );
}

これが Angular の src/app/about/about.html を置き換える React Component 本体になります。

最後に src/app/about/about.ts を編集します。

import { Component, Input, ElementRef, OnInit, OnDestroy } from '@angular/core';
import { Pokemon } from '../model/pokemon';

import { AboutComponent } from './AboutComponent';
import { AboutComponentProps } from './AboutComponentProps';

import React from 'react';
import ReactDOM from 'react-dom/client';

@Component({
  selector: 'app-about',
  imports: [],
  templateUrl: './about.html',
  styleUrl: './about.css'
})
export class About implements OnInit, OnDestroy {
  @Input() pokemon: Pokemon | undefined;

  private root: ReactDOM.Root | undefined

  constructor(private el: ElementRef){}

  ngOnInit() {
    const props: AboutComponentProps = {
      pokemon: this.pokemon
    };

    this.root = ReactDOM.createRoot(this.el.nativeElement);
    this.root.render(React.createElement(AboutComponent, props));
  }

  ngOnDestroy(): void {
    if (this.root) {
      this.root.unmount();
    }
  }
}

ngOnInit で次のように記述することで Angular で定義している about.html ではなく、 今作成した React コンポーネントである AboutComponent を View として使うことができるようになります。

    this.root = ReactDOM.createRoot(this.el.nativeElement);
    this.root.render(React.createElement(AboutComponent, props));

それでは npm run build さらに npm start して動作を確認します。

AboutComponent

まだ(たとえ Pokemon List からポケモンを選択しても) unknown としか表示できませんが、 React コンポーネントの AboutComponent を表示することができました。

選択された pokemon を AboutComponent に反映

現在のコードでは、単に AboutComponent が生成された時点で about.ts が保持している pokemon の値(それは undefined )を受け渡しているだけです。 about.ts で pokemon の値が変更されたときに AboutComponent にそれを通知して変更した pokemon の値を反映させる必要があります。

まず AboutComponent において pokemon は変化する値なので、useState を導入します。 さらに、pokemon 値が変更されたときにそれを検知できるようにオブザーバーをセットします。 ( useEffect を使う。)

src/app/about/AboutCompoennt.tsx を修正:

import React, { useState, useEffect }  from 'react';
import { Pokemon } from '../model/pokemon';
import { AboutComponentProps } from './AboutComponentProps';

export const AboutComponent = (props: AboutComponentProps): React.ReactElement => {

  const [pokemon, setPokemon] = useState<Pokemon | undefined>(props.pokemon);

  useEffect(() => {
    props.setPokemonObserver((newPokemon) => {
      setPokemon(newPokemon);
    });
  }, []);

  ...

わざわざ useEffect を使う理由

useEffect を次のように使えば、つまり useEffect(sectup, dependencies?) の dependencies に [] として何も指定しなければ:

useEffect(() => { 処理a }, []); 

処理a はこのコンポーネントが初期化されたときに一度だけ実行されます。

現在の処理内容なら何度実行しても同じなので、useEffect なしでも構わないのですが、 何度やっても同じ処理を毎回実行するのは無駄で初回に一度だけ実行するようにしています。

まだ prosp: AboutComponentProps には setPokemonObserver 関数は定義していないのでその定義を src/app/about/AboutComponentProps.ts に追加:

import { Pokemon } from '../model/pokemon';

export type PokemonObserver =  (newPokemon: Pokemon | undefined) => void;

export interface AboutComponentProps {
  pokemon: Pokemon | undefined;
  setPokemonObserver: (observer: PokemonObserver) => void;
}

PokemonObserver タイプも追加しました。

あとは about.ts で値の変更があったら通知するようにします。

import { Component, Input, ElementRef, OnInit, OnDestroy, OnChanges } from '@angular/core';

...

import { AboutComponentProps, PokemonObserver } from './AboutComponentProps';

...

export class About implements OnInit, OnDestroy, OnChanges {

  private pokemonObserver: PokemonObserver | undefined;
  
  ...

  ngOnInit() {
    const props: AboutComponentProps = {
      pokemon: this.pokemon,
      setPokemonObserver: (observer: PokemonObserver): void => {
        this.pokemonObserver = observer;
      }
    };
    
    ...
  }

  ngOnChanges() {
    if (this.pokemonObserver) {
      this.pokemonObserver(this.pokemon);
    }
  }
  
  ...
}

@Input() pokemon しているこの pokemon 値が変化すると、 ngOnChanges が呼ばれるので、 そのタイミングで this.pokemonObserver(this.pokemon) で変化した pokemon 値を通知しています。

npm start して動作を確かめます。

About Component

Pokemon List で選択したポケモンが AboutComponent に反映できるようになりました。

あとは about.html にあわせてポケモンの詳細情報(といっても名前とレベルだけですが)を表示できるようにします。

src/app/about/AboutComponent.tsx を修正:

...

  /*
  return (
    <div>
      {pokemon?.name ?? 'unknown'}
    </div>
  );
  */
  return (
    <div style={{border: 'solid 1px #999', padding: '0.5em'}}>
      <div>
        About{'\u00A0'}
        <span style={{fontStyle: 'italic'}}>
          {pokemon?.name ?? 'unknown'}
        </span>
      </div>
      <dl>
        <dt>name:</dt>
        <dd>{pokemon?.name ?? 'unknown'}</dd>
      </dl>
      <dl>
        <dt>level:</dt>
        <dd>{pokemon?.level ?? 0}</dd>
      </dl>
    </div>
  );
}

...

JSX において半角スペースを作り出すには {'\u00A0'} と記述する。

npm start して作動を確認します。

About Component

できました。

ここまで完成したら src/app/about/about.html はもう不要なので削除して構いません。

まとめ

今回は About コンポーネントを React にしました。 次回 【その3】 では、List コンポーネントを React にします。