React使いだった自分が未知のAngularでプロダクト開発に貢献できるようになるまでの話
はじめに
現在EASMの開発をしております加藤です。
先日 新規サービスのフロントエンド技術選定 ~Angularをメインに据えてみたその後~ – SSTエンジニアブログ の記事にもある通りEASMのフロントエンドはAngularを用いて開発を行っています、採用の経緯などの詳細はそちらをご覧ください。
SSTにジョインした時点でAngularを用いた開発をしていることは把握していたのですが、元々筆者はAngularをまったく触ったことがなく、とにかくAngularでのコンポーネント開発手順やお作法を覚えることが急務でした。
当初の印象としてAngularはReact (Next.js)より大規模なアプリケーションを作成するためのフレームワークという印象を持っていました。
自分にAngularを使いこなせるのだろうかと、必要な学習量などの想像もつかず怯えていたのですが、思ったより筆者の過去の知見の組み合わせでスムーズに理解を進めることができたので、その体験をシェアしようと思います。
恐らく、バックエンドの開発とテンプレートエンジンの組み込みをやったことがある方であれば、Angularの学習コストはReactやVue.jsなどと比べても大差ない印象を持ったので、選定の候補に入れてみては?という記事です。
本来SPAとして比較するとなるとAngularの対をなすのはReactにルーティングやスタイリングなどの要素パッケージングしているNext.jsになるかと思うのですが、当方があまりNext.jsを触っていないこともあり、ひとまず双方でコンポーネントを1つ作り上げるまで(フレームワークとしてではなくライブラリとして)の範囲での記事となることをご了承いただけますと幸いです。
なお、ReactとAngularの双方の違いや個人的な感想は当記事にも記載しておりますが、具体的な比較をしてみた系の記事はネット上にたくさん公開されているので、それらを参照いただけますと幸いです。
※本記事における意見は、筆者の個人的な意見であり、所属団体や関与するプロジェクト等の意見を代表するものではありません。
結論「ReactのクラスコンポーネントとSpring BootのDIを足して割った感じか」
いや何を言い出しているんだコイツと思いましたよね?正直私もそう思います。
しかし、Angularのハンズオンの記事やAngular公式のGetting startedを読み進めていくと、学習コストが高いと謳われているわりにはReactのクラス構文とSpring Bootなどを用いた開発をしていた身としては見出しの通りの感想を持ち「なんだそれだけか……」と拍子抜けしました。
Reactのクラスコンポーネントとは
Reactと言われて多くの方が想像されるのは関数型コンポーネントとhookによる実装を想像されるかと思います。
import { useState, createRoot } from 'react';
import { createRoot } from 'react-dom/client';
export const IcrementButtonComponent = (props: Props) => {
const [count, setCount] = useState(0);
const increment e => setCount(count + 1);
return <div>
<button onClick={increment}>クリックしてください</button>
<div>{{ count }}回クリックしました</div>
</div>
}
// DOMとして描画
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<IcrementButtonComponent />);
しかし、Reactが最初から関数コンポーネントで useState
によるステートの管理や useEffect
や useContext
等のhook関数が使えたわけではありませんでした。
React v16.8がリリースされるまで関数コンポーネントの役割としては、親コンポーネントから渡されるpropsを受け取って描画することしかできませんでした。
では、コンポーネントにステートを持たせたり、ライフサイクルメソッドを使用したい場合はどうしていたかというと関数コンポーネントではなくクラスコンポーネントを使用していました。
※さらに遡ると React.createClass
なるものがありますが、それに関しては触ったことがないので省略します。
import { Component } from 'react';
import { createRoot } from 'react-dom/client';
export class IcrementButtonComponent<Props, State> extends Component {
constructor(props){
this.props = props;
this.state = { count: 0 };
}
increment(e) {
this.setState({
count: this.state.count + 1,
});
}
render() {
const { count } = this.state;
return (
<div>
<button onClick={ this.increment } data-count={ count }>
クリックしてください</button>
<div>{ count }回クリックしました</div>
</div>
);
}
}
// DOMとして描画
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<IcrementButtonComponent />);
親から渡された値は props
でコンポーネント自体が保持するステートは state
プロパティで保持するのが基本的なクラスコンポーネントの基本となっています。
後にフロントエンドエンジニアとの連携の兼ね合いで関数コンポーネントにシフトしていきましたが、元々バックエンドエンジニアでJavaを書いていたこともあったためか、個人的にはReactのクラスコンポーネントを気に入って使用していました。
Angularのコンポーネントクラスについて
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
@Component({
selector: 'icrement-button',
standalone: true,
imports: [CommonModule],
// templateUrlでテンプレートのパスを指定するのが慣例だが、記事用で直接templateプロパティにhtmlを記載している
// templateUrl: './app.component.html',
template: `<div>
<button (click)="increment()" [attr.data-type]="count">
クリックしてください
</button>
<div>{{ count }}回クリックしました</div>
</div>`,
styleUrl: './app.component.scss'
})
export class IcrementButtonComponent {
count = 0;
increment() {
this.count = this.count + 1;
}
}
// DOM として描画する
// 描画するエリアについてはhtmlファイル側で指定
// 要素名は@Componentデコレーターのselectorで決まる
// ex) <icrement-button></icrement-button>
bootstrapApplication(IcrementButtonComponent)
.catch((err) => console.error(err));
はじめは「 @Component
ってなに!?JavaScriptにもJavaみたいなアノテーションってあるのだっけ」と思い調べたところTypeScriptの デコレーター というものだったという新しい発見はあったものの、それさえわかってしまえば基本的にはよくあるクラスオブジェクトでコンポーネントが定義されていて拍子抜けしました。
むしろReactだとコンポーネントが持つ状態(ステート)を更新する場合は setState
関数を経由する必要がありましたが、Angularではクラスのメンバ変数を更新するだけで再描画されるシンプルな作りになっています。
またReactにおける props
による親コンポーネントからの値の受け渡しに関しても、上記のサンプルコードには記載がないですがAngularでは @Input
というデコレーターを用いたメンバ変数を定義することで受け取れるようになります。
Reactでいう onClick
が (click)
などの違いだったり、メンバ変数を参照するディレクティブやバインディングするものに対してカッコが付いていたりと、細かな違いはありつつも、そこは慣れかなと思いました。
Spring Bootと同様のDIがAngularでもできる
React本体は直接DIのようなものは用意されていないですが、関数コンポーネントの useContext
や useReducer
を用いてレンダリング以外のロジックを切り出すことはできます。
一方Angularでは外部ストアからのデータフェッチやAngularの各種モジュールを必要に応じてコンストラクタ引数を用いて依存性の注入ができます。
@Injectable({ providedIn: 'root' })
class HogeService {
async getData() {
return Promise.resolve([0, 1, 2, 3]);
}
}
@Component({
// 中略
providers: [HogeService],
})
export class IcrementButtonComponent {
constructor(private hogeService: HogeService) {
hogeService.getData()
.then(console.log);
}
// 中略
}
一方、Spring BootでのDIは方法がいくつかありますが、コンストラクタを用いたDIがよく似ているかと思います。
@Controller
public class AppController{
private final HogeService hogeService;
@Autowired
public AppController(HogeService hogeService){
this.hogeService = hogeService;
}
}
過去に自分が触ってきたSpring Bootでもコンストラクタを用いたDIを実装していましたので、ここでも拍子抜けしたのと同時にこれであれば自分でも直ぐにプロダクト開発に参画してすぐにでも実装を進められそうと安心したのを覚えています。
おわりに
以上簡単にではありますが、私が何も理解していないAngularを使ってプロダクト開発に貢献するまでの話を紹介させていただきました。
ReactとAngularそれぞれの変更検知から再描画までの仕組みや、Reactの <div>HOGE</div>
みたいなJSXと呼ばれる糖衣構文が基本的に React.createElement('div', null, 'HOGE')
のように脱糖されているのに対してAngularはどのようにコンパイル(トランスパイル)されているかなど、より踏み込んだ話ができるようになった際には改めてブログ記事として紹介できたらと考えています。