Home About Contact
Angular

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

次のような Angular アプリケーションがあったとして、これを段階的に React に移行するためのコードメモ。

Pokemon App

【その1】では、この簡単なウェブアプリを Angular でゼロから構築していきます。

設計方針

次のように3つのコンポーネントから構成されたアプリにします。

Pokemon App Design

ベースの作成

環境の確認:

$ node --version
v22.17.0
$ npm --version
10.9.2

それでは Angular アプリのベースを作成します。

ng コマンドをグローバルにインストールするのが普通だと思われますが、 ここでは作業用のディレクトリに ng コマンドを入れて、そこで Angular アプリのベースを作成します。

$ mkdir tmp
$ cd tmp
$ npm install @angular/cli@20

Angular バージョンは 20 を使います。

ng コマンドを入れたので、アプリのベースを作成:

$ npx ng new pokemon

質問がいくつか出るので適当に答えます。

$ npx ng new pokemon
✔ Do you want to create a 'zoneless' application without zone.js (Developer Preview)? No
✔ Which stylesheet format would you like to use? CSS             [ https://developer.mozilla.org/docs/Web/CSS]
✔ Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? No

./pokemon/ にプロジェクトのベースができました。

./tmp/pokemon/ の位置のまま開発を続けると気持悪いので適当な場所に ./pokemon ディレクトリ移動して作業します。

ので、これを使って開発をしていきます。

とりあえずプロジェクトに入って念のため npm install しておきます。

$ cd pokemon
$ npm install

これでベースの用意は完了です。

起動して動作確認

さっそく npm start してブラウザで http://localhost:4200/ にアクセスして動作を確認します。

List コンポーネントを作成

$ npx ng generate component list

src/app/list/ ディレクトリ内に List コンポーネントのひな形が用意されました。

src/app/list/list.html を次のように編集します:

<div>Pokemon List</div>
<div>
  <ul>
    <li>Pikachu</li>
    <li>Squirtle</li>
  </ul>
</div>

まずは小手試しとして、 これ(app-list)をそのまま現在の app-root の代わりに使います。

src/index.html を編集:

<body>
  <!--<app-root></app-root>-->
  <app-list></app-list>
</body>

src/main.ts を編集:

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
//import { App } from './app/app';
import { List } from './app/list/list';

bootstrapApplication(List, appConfig)
  .catch((err) => console.error(err));

ここまで編集したら npm run build してエラーがないか確認。 問題なければ npm start して動作確認します。

List

意図通りになりました。

Container コンポーネントの追加

設計上 List コンポーネントは Container コンポーネントの子として配置することになっていました。 ここで Container コンポーネントを作成して、 List コンポーネントはその子コンポーネントとして配置するようにします。

$ npx ng generate component container

src/app/container/container.html を編集:

<div>
  <app-list></app-list>
</div>

app-list 要素を使えるように src/app/container/container.ts を編集:

import { Component } from '@angular/core';
import { List } from '../list/list';

@Component({
  selector: 'app-container',
  imports: [List],
  templateUrl: './container.html',
  styleUrl: './container.css'
})
export class Container {

}

List をインポートして、@ComponentimportsList を追記。

src/index.html を編集:

<app-list></app-list>
<body>
  <!--<app-root></app-root>-->
  <!--<app-list></app-list>-->
  <app-container></app-container>
</body>

src/main.ts を編集して、index.htmlapp-container を使えるようにします。

//import { List } from './app/list/list';
import { Container } from './app/container/container';

bootstrapApplication(Container, appConfig)
  .catch((err) => console.error(err));

npm run build して動作確認します。

List

先程と見た目は同じですね。

モデルを用意

ポケモン名のリストを表示して、それを選択すると詳細情報が表示されるアプリなので、 現在のようにポケモン名を List コンポーネントに直接ハードコーディングしないで、 モデルとして別管理することにします。

まずは Pokemon タイプを作成します。

src/app/model/pokemon.ts を作成:

export type Pokemon = {
  name: string;
  level: number;
  selected: boolean;
};

Pokemon タイプの定義だけでなく、 デフォルトのポケモンリストデータも作成しておきます。

export const DefaultPokemonList: Pokemon[] = [
  {
    name: 'Pikachu',
    level: 100,
    selected: true
  },
  {
    name: 'Squirtle',
    level: 90,
    selected: false
  },
  {
    name: 'Charamander',
    level: 120,
    selected: false
  },
];

Container にモデルを配置、それを List コンポーネントに渡して表示

作成した Pokemon モデルを Container に配置します。

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

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

...

export class Container {
  pokemonList: Pokemon[] = [...DefaultPokemonList];
}

さらに src/app/container/container.html を編集して app-listpokemonList を追加:

<div>
  <app-list [pokemonList]="pokemonList"></app-list>
</div>

app-list つまり List コンポーネントでこの pokemonList を受け取って描画できるようにします。

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

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

@Component({
  selector: 'app-list',
  imports: [],
  templateUrl: './list.html',
  styleUrl: './list.css'
})
export class List {
  @Input() pokemonList: Pokemon[] = []
}

変更が大きいので、ここらで npm run build してビルドできるか確認しましょう。

問題がなければ、 src/app/list/list.html を編集して受け取った pokemonoList をレンダリングできるようにします。

<div>Pokemon List</div>
<div>
  <!--
  <ul>
    <li>Pikachu</li>
    <li>Squirtle</li>
  </ul>
  -->
  <ul>
    <li *ngFor="let pokemon of pokemonList">
      <span>{{pokemon.name}}</span>
      <span *ngIf="pokemon.selected"> *</span>
    </li>
  </ul>
</div>

ただ、ここで npm run build すると警告がでます。

▲ [WARNING] NG8103: The `*ngFor` directive was used in the template, but neither the `NgFor` directive nor the `CommonModule` was imported.

CommonModule を import しなければいけないようです。

src/app/list/list.ts で CommonModule をインポートします。

import { CommonModule } from '@angular/common';

...

@Component({
  ...
  imports: [CommonModule],
  ...
})

これで再度 npm run build すると警告はなくなりました。 npm start して動作確認します。

Container and List

意図通り DefaultPokemonList の内容がリストできるようになりました。

List コンポーネントで選択中のポケモンをチェンジする振る舞いを追加

今は単に表示しているだけで、選択中のポケモンに * はついていますが、 これを別のポケモンに変えたりはできません。 この選択中のポケモンを変更する、という振る舞いを List コンポーネントに追加します。

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

...
    <li *ngFor="let pokemon of pokemonList">
      <!--<span>{{pokemon.name}}</span>-->
      <button (click)="onClick(pokemon.name)">{{pokemon.name}}</button>
      <span *ngIf="pokemon.selected"> *</span>
    </li>
...

span でポケモン名をレンダリングしていた部分を button に変更して、それを click したら onClick 関数を 呼ぶように変更しました。

まだ onClick 関数は用意していないので、 src/app/list/list.ts に追加します。

...

  @Input() pokemonList: Pokemon[] = []

  onClick(name: string): void {
    /*
    // これでも機能するとは思うが...
    this.pokemonList.forEach((it)=> {
      it.selected = (it.name===name) ? true : false;
    });
    */

    this.pokemonList = this.pokemonList.map((it)=> {
      return {
        ...it,
        selected: (it.name===name) ? true : false
      }
    });
    
    const pokemon = this.pokemonList.filter((it)=>it.name===name)[0];
    console.log(pokemon);
  }

...

クリックされたポケモンを選択状態に、それ以外を非選択状態にしています。 さらにそのあと、選択されたポケモンをコンソールに出力しています。

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

Container and List

ポケモン名のボタンをクリックすることで選択中のポケモンを変更できるようになりました。

ポケモン詳細情報を表示する About コンポーネントを追加

$ npx ng generate component about

このコンポーネントは pokemon: Pokemon を受け取って表示するつもりなので、 src/app/about/about.ts を次のように編集します:

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

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

@Input pokemon としてポケモンオブジェクトを受け取ることができるようにしています。

この pokemon を表示するために、 src/app/about/about.html を次のように編集:

<div style="border: solid 1px #999; padding: 0.5em;">
  <div>
    About
    <span style="font-style: 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>

それではこの About コンポーネントを Container の子コンポーネントして配置します。

src/app/container/container.html に About コンポーネントを追加:

<div>
  <app-list [pokemonList]="pokemonList"></app-list>
  <app-about [pokemon]="pokemon"></app-about>
</div>

app-about が使えるように src/app/container/container.ts を編集:

import { Component } from '@angular/core';
import { List } from '../list/list';
import { About } from '../about/about';
import { Pokemon, DefaultPokemonList } from '../model/pokemon';

@Component({
  selector: 'app-container',
  imports: [List, About],
  templateUrl: './container.html',
  styleUrl: './container.css'
})
export class Container {
  pokemonList: Pokemon[] = [...DefaultPokemonList];
  pokemon: Pokemon | undefined;
}

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

Container List About

About コンポーネントは出現していますが、 List コンポーネントでポケモンを選択しても、まだ About には反映されません。

その振る舞いを追加します。

List コンポーネントの src/app/list/list.ts を編集:

import { Component, Input, Output, EventEmitter } from '@angular/core';

...

export class List {
  ...
  @Output() onPokemonChange = new EventEmitter<Pokemon>();

  ...

  onClick(name: string): void {
    ...
    const pokemon = this.pokemonList.filter((it)=>it.name===name)[0];
    //console.log(pokemon);
    this.onPokemonChange.emit(pokemon);
  }
}

元のコードは単に選択中の pokemon を console.log していただけですが、 これを this.onPokemonChange.emit(pokemon); して親コンポーネントに送っています。

親コンポーネント(Container)でこの pokemon を受信できるように、 src/app/container/container.html を編集:

...
  <app-list
    [pokemonList]="pokemonList"
    (onPokemonChange)="onPokemonChange($event)">
...

まだ onPokemonChange 関数を定義していないので、 src/app/container/container.ts に定義する。

...
  onPokemonChange(pokemon: Pokemon): void {
    this.pokemon = pokemon;
  }
...

動作を確認します。

Container List About

選択したポケモンを About コンポーネントに表示できるようになりました。

まとめ

React へ移行する Angular アプリはこれで完成です。 【その2】 では一部のコンポーネントだけを React に移行していきます。