この記事は MICIN Advent Calendar 2022 の18日目の記事です。
前回は酒井さんの記事でした。
目次
- はじめに
- 前提として
- 実装の仕方
- 実装で詰まったポイント
- まとめ
はじめに
どうもどうもオンライン診療curonのエンジニアをしてます影山です、以後お見知り置きを~
さてさてMICIN Advent Calendar 2022 の前回は酒井さんの「今よりもっと早く、質の高いプロダクト開発をめざして~Confluence&Jiraでモダンな開発プロセスの構築~」でした。まだの方は是非是非こちらも合わせて読んでみてください。
18日目ということで本日は「AngularにReactを乗せて実装してみた」について記事を書いていこうと思います。
前提として
MICINにはさまざまなプロダクトがありそれぞれ色々な技術が使われているのですが、今回紹介する実装を乗せているプロダクトはcuron(オンライン診療サービス)に実装していましてMICINの創業当時からあるプロダクトで特に歴史の古いプロダクトでありそれだけ技術的には古い部分が多くあります、、
その中でも実装する上で特に学習コストを上げているのがAngularとRxJSです。curonにおいてはPromiseで十分代替可能でRxJSの恩恵が少ないかつ、MICINのフロントエンド実装は基本的にReactがメインとなっていてAngularが使われているのはこのプロダクトだけになってしまいました、今このプロダクトのためだけにAngularのフルスタックフレームワークとしての記述の仕方やRxJSのObservableなどの技術をキャッチアップするはなかなか大変で他の部署の方がこのプロダクト触るのは難しく負債になってしまっている現状です。
他プロダクトと技術スタックを合わせて、curonでもReactを採用したいのですが…創業当初からあるプロダクトだけあり実装の規模も大きく一遍に作り替えるのは現実的ではありません。
そこで色々と検討した結果Angular内でReact化をしていきコンポーネント毎に徐々にReact化を進める方法です。
実装の仕方
さてさてだらだらと前提として色々述べてきましたがどうやって実装するかが気になるところだと思います。
一言で言うと「Angularでdivを作って、そこにDOM操作でReactコンポーネントを出力する」と言う方法です、ここからはコードを例に上げながら説明できればと思います。
全体像の実装としては以下になり
package.json
"react": "^18.0.0",
"react-dom": "^18.0.0",
"@types/react": "^18.0.5",
"@types/react-dom": "^18.0.1",
//その他にもMUIなど使いたいReactのライブラリーを追加
Angularコンポーネント
import {
AfterViewInit,
Component
EventEmitter,
Input,
OnChanges,
OnDestroy,
Output,
} from '@angular/core'
import React from 'react'
import { createRoot, Root } from 'react-dom/client'
//表示したいReactのコンポーネントとそのコンポーネントのPropsをimport
import { Demo, Props } from './demo'
//styleThemeなど全体で統一したいもの
import { RootProvider } from '@app/react/helpers/RootProvider'
@Component({
selector: 'confirmation-modal-content-react-component',
template: '<div [id]="rootId"></div>',
})
export class ReactDemoComponent implements OnChanges, AfterViewInit, OnDestroy {
//一つの画面で同じReactコンポーネントを呼び出したい場合はこのIDをユニークにする必要があります
public rootId = 'demo-react-component'
private hasViewLoaded = false
private root: Root
//Angularの呼び出し部で指定した@Input,@Outputの呼び出し
@Output() hoge = new EventEmitter<void>()
@Input() value: Value
public ngOnChanges() {
this.renderComponent()
}
public ngOnDestroy() {
this.root.unmount()
}
public ngAfterViewInit() {
//divが表示される前にrenderComponentが流れると困るのでここで制御
this.hasViewLoaded = true
this.renderComponent()
}
private renderComponent() {
if (!this.hasViewLoaded) {
return
}
const props: Props = {
hoge: () => this.hoge(),
}
const container = document.getElementById(this.rootId)!
if (!this.root) {
this.root = createRoot(container)
}
this.root.render(
// App.tsxがないため全体に聞かせたいstyleThemeやcontextは全てRootProviderに定義する
React.createElement(RootProvider, {
//Reactのコンポーネントとpropsを指定する
children: React.createElement(Demo, props),
}),
)
}
}
AngularからのReact呼び出し部はこのようなテンプレートを記述します!
Reactのコンポーネント側では特に特別な記載なくコンポーネントを作っていただくで大丈夫です。
このようにReact.createElement
を使うことによってReact呼び出すことができるのは同じJSだからこそできることですね!!
実装で詰まったポイント
実装の仕方で書いたように書けばある程度動くのですが、やはりいくつか詰まるポイントもあったのでご紹介できればと思います。
App.tsxがない、、
普段create-react-appやnext.jsなどで開発する際には共通のテーマやContextのプロバイダーなど、コンポーネント全体に適応したいコードを記述する場所なのですが、コンポーネント毎にReact.createElement
をしているためApp.tsxを作成することができません、、
では全体にThemeなどを指定したいときはどうするかとなるのですが今回はcreateElement
をネストすることによって一つ目に全体で共通としたいものをテンプレートとして全てのコンポーネントで共通して記述するようにしました。
// App.tsxがないため全体に聞かせたいstyleテーマやcontextは全てRootProviderに定義する
React.createElement(RootProvider, {
//Reactのコンポーネントとpropsを指定する
children: React.createElement(Demo, props),
}),
)
Reactからナビゲーションがうまくいかないことがある
ナビゲーションは基本的に Angular側で関数として作成してそれをReactに渡して画面遷移を行うのですが、
Angularの外部でイベントが発生した際に変更の検知がNgZoneを指定しないと遷移ができないという問題が発生する場合があります。
Angularのナビゲーションする場所に下記のような記述をして対応しました。
this._ngZone.run(() => {
this.router.navigate(path, { queryParams, ...extras })
})
Reactコンポーネント間でグローバルな値を持つのが難しい
App.tsxでも少し書きましたがコンポーネントごとにReact.createRoot をしていて、 Context の値を共有できないのでコンポーネント間でのデータの共有が難しいです。プロダクトのバックエンドAPIの通信を当初はReactQueryを使って行おうと思っていましたが、App.tsxにProvider
を持つ必要があり断念、、、
色々調べていくとSWRを使えばProvider
を持つことなくキャッシュでいい感じにデータを共有できるので基本的に共通の値に関してはSWRで管理することとしています。
まとめ
いやぁ~AngularがいいかReactがいいか的なことはあまり触れないで書いてきましたが、Reactでの開発はなんか安心感がありますよね。AngularやRxjsも使用用途によっては強い力を発揮しますが、やはりReactで実装できるだけで他のプロダクトの開発と行き来した時も同じようにかけるのは開発体験としてとてもいいです。
簡単にでしたがAngularからReactのコンポーネントの呼び方をご紹介しました、まだまだ現状数カ所しかプロダクトをReact化できていませんが将来的には全てReact化して行きたいですね。また進捗や詰まったポイントなどあれば記事にはして行きたいと思います。
参考にした記事
Integrating React Components into an Angular 2+ Project
MICINではメンバーを大募集しています。
「とりあえず話を聞いてみたい」でも大歓迎ですので、お気軽にご応募ください!
MICIN採用ページ:MICIN 採用情報