Angular から React への移行【その2】 では @Input のみがある単純な Angular コンポーネントを React コンポーネントに移行しました。 今回は、@Input だけでなく @Output も存在する Angular コンポーネントを React に移行します。
前回の【その2】 で、 About コンポーネントを React にしたので それにならって移行していきます。
まず ListComponentProps を src/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 があるので、 ListComponentProps に pokemonList, setPokemonListObserver を用意しました。
そして list.ts には @Output() onPokemonChange があるので、 それに対応するために onPokemonChange も用意しました。
次に、 ListComponent を src/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>
);
}
pokemon が pokemonList に変わっただけで、前回作成した 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 を修正していきます。
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 して動作を確認しましょう。
かきかけです。