この記事は2022Insurtechラボアドベントカレンダーの21日目の投稿となります。
Amplify Studio最大の売りは、Figmaデザインを迅速にコード化(以下、コード化で生成されたReactコンポーネントを「UIコンポーネント」と呼ぶ)できることにあると思っています。ところが、UIコンポーネントに業務ロジックを直接書き込むと、途端に「迅速」ではなくなります。
Amplify公式サイトではprop「overrides」の利用してUIコンポーネントを直接編集しない手法が提案されており、試してみると非常にうまくできたため、以下で紹介したいと思います。
UIコンポーネントにロジックを搭載することによる弊害
まずは、UIコンポーネントを直接編集することでどのような弊害につながるのかを確認しておきます。
UIコンポーネントを直接編集する場合の流れは以下のようになります。
これによる弊害は以下3点です。
業務ロジックが消える
コード化(amplify pullコマンド)では、生成済みのUIコンポーネントを容赦なく上書きします。
消えた業務ロジックをGitでマージし直せば解消するのですが、この作業は煩雑で、デグレードの危険性があります。だんだんとFigmaによるデザイン変更が嫌になってきます(そしてUIも手で書き換え始める)。
UIコンポーネントはメンテナンスし難い
生成されたUIコンポーネントにはこんなコードがよく出てきます。
return (
<View>
<View>
<Flex>
<View>
どうでしょうか?個人的には、
・ネストが深い
・Amplify Studioが良かれ(?)と思って設定してくる「効いていないプロパティ」が沢山ある(結局どれが効いてるの?)
・どういう考え方で差し込まれたコンポーネントなのかわからない(デザインの構造が見えない)
と思っています。
要は読みにくいコードということです。
特に、最後のデザインの構造が見えてこないのが致命的で、人間が継続してメンテナンスする気になりません。
UIコンポーネントはFigma+Amplify Studio連合におまかせするのが精神衛生上も良いということです。
UIコンポーネントは編集禁止
コード化されたUIコンポーネントの先頭には以下のようなコメントが自動挿入されます。
/***************************************************************************
* The contents of this file were generated with Amplify Studio. *
* Please refrain from making any modifications to this file. *
* Any changes to this file will be overwritten when running amplify pull. *
**************************************************************************/
「このファイルへの変更はご遠慮ください」「このファイルへの変更は、amplify pullしたら上書きされます」
と書いてありますよね。我々は、ちゃんと「ご遠慮」しないといけません。
overridesを使った解決方法
では、overridesプロパティよって上記の弊害がどのように解決されるのか見ていきましょう。
基本戦略
UIコンポーネントを包むラッパーコンポーネントを定義します(コンテナコンポーネントと呼ばれるものです)。
業務ロジックはラッパーコンポーネントに定義し、UIコンポーネントにはView表示の責務だけ持たせます。
そして、業務ロジックをoverrides経由でUIコンポーネントに差し込みます。実装時にはこんな作業フローになります。
overridesを使うことで、見た目(UIコンポーネント)の変更だけでは業務ロジック(ラッパーコンポーネント)を物理的に編集しなくても済むようになります。
業務ロジック担当者のマインドは「デザイン変更はいつになったら終わるの?いつまで取り込んだらいいの??(疲」から「あ、変わったんだね。いいじゃん」になります。無駄な揉め事がなくなり、とてもよいです。
具体的な方法
Amplify Studioで生成されたUIコンポーネントには、必ず「overrides」というpropが用意されます。
(例)あるコンポーネント(MyUiComponents)の中に、firstNameという名前のコンポーネントがある場合
export default function MyUiComponents(props) {
const { overrides, ...rest } = props;
(...略...)
return (
<View
{...getOverrideProps(overrides, "MyUiComponents")}
>
<TextField
(...略...)
{...getOverrideProps(overrides, "firstName")}
></TextField>
ここで、上記「firstName」に値を初期値「太郎」を設定するやり方をoverrides使わない・使うケースで比較します。
overridesを使わない場合(UIコンポーネント直接変更)
UIコンポーネントに「defaultValue」を定義し、「太郎」を値として設定します。
export default function MyUiComponents(props) {
(...略...)
return (
<View
{...getOverrideProps(overrides, "MyUiComponents")}
>
<TextField
(...略...)
defaultValue: "太郎",
{...getOverrideProps(overrides, "firstName")}
></TextField>
overridesを使う場合
UIコンポーネントを包むラッパーコンポーネントを用意します。ここでは、「MyUiComponentsWrapper」とします。
export default function MyUiComponentsWrapper(props) {
return (
<MyUiComponents
overrides={{
"firstName": {
defaultValue: "太郎"
}
}}
></MyUiComponents>
);
}
UIコンポーネント側で定義された名前(上記では「firstName」)を指定して、上書きしたいpropを指定します。
overridesを使わない方法に比べ、ラッパーコンポーネントを定義する手間は増えますが、書き方自体はシンブルで、UIコンポーネント直接編集による面倒臭さと比べたら遥かに楽です。
UIコンポーネント側で値が設定済みのpropであっても、overridesで上書きした値が優先されます。
つまり、propで定義できるものは全て新規定義&上書き可能です。overridesを使った書き方をいくつか挙げてみます。
Stateを設定する
export default function MyUiComponentsWrapper(props) {
const [name, setName] = useState("");
...
return (
<MyUiComponents
overrides={{
"firstName": {
defaultValue: name
}
もちろん、ReduxやRecoilから取得したStateも指定可能です。
JavaScript関数の呼び出し
export default function MyUiComponentsWrapper(props) {
const myfunc = (name) => { /* 業務ロジック */ };
...
return (
<MyUiComponents
overrides={{
"firstName": {
defaultValue: myfunc('太郎')
}
イベントハンドラ定義
export default function MyUiComponentsWrapper(props) {
const myfunc = (event) => { /* 業務ロジック */ };
...
return (
<MyUiComponents
overrides={{
"firstName": {
onBlur: (event) => {myfunc(event)}
}
任意の子要素追加(children)
あまりやらないかもしれませんが、childrenもpropの一つなので、children経由で子要素を差し込むことができます。
下記の例は、firstNameがSelectFieldだとして、そこにoptionを差し込むパターンです。
export default function MyUiComponentsWrapper(props) {
...
return (
<MyUiComponents
overrides={{
"firstName": {
children:
<>
<option key='k1' value='1'>太郎</option>
<option key='k2' value='2'>次郎</option>
</>
}
overridesのネスト
下記の例は、”firstName”が示すコンポーネントの中に、”Button”という名前のコンポーネントがネストされていた場合、そのネストされたButtonをsubmitボタンに変更します。
export default function MyUiComponentsWrapper(props) {
const myfunc = (name) => { /* 業務ロジック */ };
...
return (
<MyUiComponents
overrides={{
"firstName": {
overrides: {
"Button": { type: "submit" }
}
}
overrides使用時の注意点
overridesを使用するにあたり、陥りやすい罠があります。実際に遭遇したものを挙げておきます。
Figmaで定義するコンポーネント名は英数字
例えば、「誕生日」という名前のTextをFigmaで定義したとします。これをコード化すると、overridesは以下ようになります。
<Text
{...getOverrideProps(overrides, "\u8a95\u751f\u65e5")}
そうです。日本語はUnicodeエスケープされた文字列としてプロパティ名にセットされます。
もちろん、これをoverridesで指定することも可能なのですが、何の項目なのかわかりません。
コンポーネント名は英数字でわかりやすい命名にした方が良いです。
variant要素のoverrides
レスポンシブ対応を実現する際にvariantの仕組みを用いた場合、「small」「large」ごとに見え方を変えることができます。
このとき、例えばoverridesを使って、「small」に定義されたwidthだけを変えることはできるでしょうか?
残念ながらできません。overridesで上書きできるのは、「small」「large」などの中から、どのbreakpointを使用するのか確定した後のpropに対してのみです。つまり、「small」「large」に対して同じ値のwidthをoverridesで上書きすることはできますが、「small」だけというような指定はできません。
breakpointを使用したレスポンシブ対応のやり方は以下を参照ください。
コンポーネントを動的に消す
propを追加したり、上書きする機能ですので、対象のコンポーネントそのものを消すことはできません。
やりたければ「display: none」で隠し表示するなど工夫が必要です。
考え方を変えて、UIコンポーネントを動的に「消す」のではなく、デフォルトは消しておき、必要に応じて「動的に追加する」ことで消えた状態を表現する方法もあると思います。
例えば、「入力チェックOKの時には”NG”アイコンを消す」は、「入力チェックNGの時だけ”NG”アイコンを出す」と読み替えるイメージです。
コンポーネントを動的に追加する(子要素以外)
childrenを使えば子要素の追加はできますが、子要素以外への追加はできません。
子要素以外に動的に追加したいのであれば、公式サイトで紹介している「Components Slot」が良いです。
Amplify Studio側でSlotを作成しておくことで、そのSlotを目掛けて任意のコンポーネントを差し込むことができます。
詳細は以下を参照ください。
https://docs.amplify.aws/console/uibuilder/slots/
まとめ
UIコンポーネントにoverridesを使って外部から業務ロジックを差し込む方法を解説しました。
UIコンポーネントを直接編集しないようにすることで、デザインの迅速なコード化を妨げなくなり、継続開発がやり易くなりますので、ぜひoverridesを活用してみてください。
overridesだけではなく、「Components Slot」「overrideItems」「Amplify Hub」などUIコンポーネントを触らずロジックを搭載する手法が他にもありますので、下記の公式サイトなどを参照してみてください。
公式サイト:
https://docs.amplify.aws/console/uibuilder/override/
AWSのブログ:
https://aws.amazon.com/jp/blogs/news/write-your-own-code-with-aws-amplify-studio/