Home About Contact
Angular , React

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

Angular から React への移行【その2】 では @Input のみがある単純な Angular コンポーネントを React コンポーネントに移行しました。 今回は、@Input だけでなく @Output も存在する Angular コンポーネントを React に移行します。

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

前回の【その2】 で、 About コンポーネントを React にしたので それにならって移行していきます。

まず ListComponentPropssrc/app/list/ListComponentProps.ts に作成:

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

export type PokemonListObserver =  (newPokemonList: Pokemon[]) => void;

export interface ListComponentProps {
  pokemonList: Pokemon[];
  setPokemonListObserver: (observer: PokemonListObserver) => void;
  onPokemonChange: (pokemon: Pokemon) => void;
}

About では @Input() pokemon がありましたが、 この List では list.ts@Input() pokemonList があるので、 ListComponentPropspokemonList, setPokemonListObserver を用意しました。

そして list.ts には @Output() onPokemonChange があるので、 それに対応するために onPokemonChange も用意しました。

次に、 ListComponentsrc/app/list/ListComponent.tsx に作成:

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

export const ListComponent = (props: ListComponentProps): React.ReactElement => {

  const [pokemonList, setPokemonList] = useState<Pokemon[]>(props.pokemonList);

  useEffect(() => {
    props.setPokemonListObserver((newPokemonList) => {
      setPokemonList(newPokemonList);
    });
  }, []);

  return (
    <ul>
      {pokemonList.map((pokemon)=> (<li>{pokemon.name}</li>))}
    </ul>
  );
}

pokemonpokemonList に変わっただけで、前回作成した AboutComponent.tsx とだいたい同じです。 pokemonList をレンダリングする JSX 部分は暫定的にポケモン名を列挙するだけにしています。

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

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

import { ListComponent } from './ListComponent';
import { ListComponentProps, PokemonListObserver } from './ListComponentProps';

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

@Component({
  selector: 'app-list',
  imports: [CommonModule],
  templateUrl: './list.html',
  styleUrl: './list.css'
})
export class List implements OnInit, OnDestroy, OnChanges {
  @Input() pokemonList: Pokemon[] = []
  @Output() onPokemonChange = new EventEmitter<Pokemon>();

  private root: ReactDOM.Root | undefined
  private pokemonListObserver: PokemonListObserver | undefined;

  constructor(private el: ElementRef){}

  ngOnInit() {
    const props: ListComponentProps = {
      pokemonList: this.pokemonList,
      setPokemonListObserver: (observer: PokemonListObserver): void => {
        this.pokemonListObserver = observer;
      },
      onPokemonChange: (pokemon: Pokemon): void => {
        this.onClick(pokemon.name)
      }
    };

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

  ngOnChanges() {
    if (this.pokemonListObserver) {
      this.pokemonListObserver(this.pokemonList);
    }
  }

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

  onClick(name: string): void {
    this.pokemonList = this.pokemonList.map((it)=> {
      return {
        ...it,
        selected: (it.name===name) ? true : false
      }
    });

    const pokemon = this.pokemonList.filter((it)=>it.name===name)[0];
    this.onPokemonChange.emit(pokemon);
  }
}

前回 about.ts に行った変更とほとんど同じです。 ただし、ポケモンを選択するアクションが起ったときに使う onPokemonChange を次のように 記述している点に注意しましょう。

  ngOnInit() {
    const props: ListComponentProps = {
      ...
      onPokemonChange: (pokemon: Pokemon): void => {
        this.onClick(pokemon.name)
      }
    };
    ...

選択中のポケモン変更通知を受け取り、もとから定義していた onClick 関数に this.onClick(pokemon.name) としてポケモン名を転送しています。

この段階で一度動作確認します。

npm run build して問題ないことを確認した上で、npm start します。

Step1

まだ、ポケモン名がリストできただけです。 ポケモンを選択できるように ListComponent を修正していきます。

ListComponent の修正

src/app/list/ListComponent.tsx を修正:

...

export const ListComponent = (props: ListComponentProps): React.ReactElement => {
  ...

  return (
    <>
      <div>Pokemon List</div>
      <div>
        <ul>
        {pokemonList.map((pokemon)=> (
           <li>
             <button onClick={(e)=>props.onPokemonChange(pokemon)}>{pokemon.name}</button>
             <span>
               {pokemon.selected ? ' *' : ''}
             </span>
           </li>
         ))
        }
        </ul>
      </div>
    </>
  );
}

button 要素にして onClick={(e)=>props.onPokemonChange(pokemon)} することで、 ポケモン名の書いたボタンをクリックしたときに list.ts に通知がいくようにしています。

これで動作を確認してみます。

Step2

ポケモンを選択すると通知が行われて、 AboutComponent の内容がそれに応じて変化するようになりました。(ここでは ポケモンリストから Charamander を選択した。) ただ、 ListComponent 上では該当ポケモンが選択状態になっていません。 この問題を修正します。

src/app/list/list.ts を編集:

  onClick(name: string): void {
    ...
    
    if (this.pokemonListObserver) {
      this.pokemonListObserver(this.pokemonList);
    }
  }

既存の onClick 関数の最後に通知処理を追加します。 this.pokemonListObserver が存在していたら、そこへ(選択状態変更済みの) this.pokemonList を渡しています。

npm start して動作を確認しましょう。

Step3

ポケモンリストで選んだポケモンが About コンポーネントにも反映され、かつ、 ポケモンリストの選択状態も意図通り更新されました。

最後に src/app/list/list.ts をリファクタリングします。

notifyChanges 関数を定義:

  private notifyChanges() {
    if (this.pokemonListObserver) {
      this.pokemonListObserver(this.pokemonList);
    }
  }

変更をオブザーバーに通知する部分を notifyChanges 関数にしました。 あとは、このコードを直接書いている部分を変更して this.notifyChanges() 関数をコールするだけにします。

  ngOnChanges() {
    /*
    if (this.pokemonListObserver) {
      this.pokemonListObserver(this.pokemonList);
    }
    */
    this.notifyChanges();
  }

  ...

  onClick(name: string): void {
    ...
    
    /*
    if (this.pokemonListObserver) {
      this.pokemonListObserver(this.pokemonList);
    }
    */
    this.notifyChanges();
  }

以上で Angular の List コンポーネントの React への移行は完成です。