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 します。

まだ、ポケモン名がリストできただけです。 ポケモンを選択できるように 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 に通知がいくようにしています。

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

ポケモンを選択すると通知が行われて、 AboutComponent の内容がそれに応じて変化するようになりました。 ただ、 ListComponent 上では選択ポケモンが変化しません。 この問題を修正します。

src/app/list/ListComponent.tsx を編集:

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

  ...

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

  const onPokemonChange = (pokemon: Pokemon): void => {
    setPokemon(pokemon);
    props.onPokemonChange(pokemon);
  };

  ...

  return (
    ...
        {pokemonList.map((pokemon)=> (
           <li>
             <button onClick={(e)=>onPokemonChange(pokemon)}>{pokemon.name}</button>
    ...
  );
}

先ほどポケモンを選択しても ListComponent 上では選択状況が変化しなかったのは、 そのタイミングで ListComponent が再描画されていなかったからです。 React に再描画を促すために、あらたに:

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

を追加して再描画が必要なタイミングで setPokemon するようにしました。 ( あらたに定義した onPokemonChange 関数内にて。)

また、以前は onClick で直接 props.onPokemonChange をコールしていましたが、 それをやめて、今回あらたに定義した onPokemonChange 関数を経由してコールすることにしました。

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

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

かきかけです。