Vue.jsは学習のしやすいフレームワークであると思う。段階的に機能の導入ができるように設計されているため、必要以上の学習コストが発生しにくいということもあるが、特定の機能(例えば、算出プロパティ)を初めて学習する場合にそれを試すことが容易であることも大きい。
特定の機能を簡単に試せることは、メリットであると共に注意も必要であると思う。特に初学者は、実際のアプリケーションの中で
- どのような状況でその機能を使えば良いのか
といった観点を把握しないままに先に進んでしまいがちで、状況に応じた適切な機能を選択できていないことが多々あるように思う。
この記事では、サンプルアプリのリファクタリングを通して、初心者がやりがちだと思うコードの書き方とその問題点を指摘していく。なお、リファクタリングは問題点に気づくための手段として行っているので、具体的なステップバイステップの安全なリファクタリング手順は記載していないことはご了承いただきたい。
目次
1. 当記事の対象読者
2. 動作環境
3. リファクタリングするサンプルアプリ
3-1. サンプルアプリの要件
3-2. リファクタリング前のコード
4. コードの問題点と改善方法
4-1. 繰り返しを含むtemplate
4-2. セットで更新しなければいけない状態
4-3. 所属がおかしな状態
4-4. 矛盾しうるプロパティ
4-5. リファクタリング後のコード
5. まとめ
6. あとがき
7. 参考にしたサイト・書籍
1. 当記事の対象読者
当記事はVue.js初心者を対象とする。一通りの機能は触ったことがあり、とりあえず動作するものは作れるが、コンポーネントが上手く書けているか分からないといった場合には参考になる箇所があるかもしれない。
前提知識としてはシングルファイルコンポーネントが理解できれば良く、Vue RouterやVuexまでは踏み込まない。
また、初心者対象とはしたものの、ある程度Vue.jsの開発経験がある方にも読んでいただき、意見や間違いの指摘等をコメントいただけるとありがたい。
2. 動作環境
サンプルアプリはvue-cliでバージョン2.6.10
のVue.jsアプリを作成、動作確認をおこなった。確認はしていないが3.0
系で変更があった機能は使っていないので、3.0
系でも問題なく動作すると思う。
2021/3/5 追記
3.0系だと一部動作しない箇所があったので修正させていただきます。
3. リファクタリングするサンプルアプリ
今回、リファクタリングの対象とするサンプルアプリは下図のようなイメージだ。
とある青果店のホームページの一角に表示するものと思って欲しい。
3-1. サンプルアプリの要件
この青果店ではりんごジュースを販売しているが、りんごジュース目当ての客が増えすぎたため、ホームページ上の抽選で当選した人にのみに販売することになった。要件は以下の通りだ。
- りんごジュースに使うりんごは日替わりで、産地や品種、値段が異なる
- Mサイズの料金を基本料金として、 Lサイズはその1.4倍の料金とする
- 抽選はメールアドレスをフォームに入力して、その結果がそのメールアドレスに対して送信される
あまり現実的ではない設定は多々あるが、説明用と割り切っていただけると幸いだ。
3-2. リファクタリング前のコード
このアプリは基本的に以下3つコンポーネントで構成されている。
-
App
...サンプルの全体 -
MyTextBox
...メールアドレスの入力フォームで使用 -
MyButton
...「抽選する」ボタンで使用
次にソースコードを示す。なるべく短くするよう努めたが、それでもやや長いため、現時点ではざっと確認する程度で構わない。
なお、CSSの記述に関しては今回の目的からは逸れること、そしてコードを短く保つために外した。ただし、スタイリングに使用したタグのclass属性は、タグの意味合いを理解する補助になるため意図的に残した。
<template>
<div id="app">
<h1>Ringo Juice</h1>
<div class="today-info">
<h4 class="today-info_title">本日のりんご</h4>
<div class="today-info_list">
<div class="today-info_item">
<span>産地</span>
{{ info.prefecture }}
</div>
<div class="today-info_item">
<span>品種</span>
{{ info.variety }}
</div>
<div class="today-info_item">
<span>Mサイズ</span>
{{ info.basePrice }}円
</div>
<div class="today-info_item">
<span>Lサイズ</span>
{{ largeSizePrice }}円
</div>
</div>
</div>
<p class="message">
りんごジュースは1日100名様限定です。<br />当選者のみが購入可能です。<br />
下記フォームにメールアドレスを入力してください。<br />抽選結果が届きます。
</p>
<div class="form">
<MyTextBox ref="email" placeholder="メールアドレス" @send-text="sendEmail" />
<MyButton isSmall @click="onClick">
抽選する
</MyButton>
</div>
</div>
</template>
<script>
import MyTextBox from '@/components/MyTextBox.vue'
import MyButton from '@/components/MyButton.vue'
export default {
name: 'app',
components: { MyTextBox, MyButton },
data() {
return {
info: { prefecture: '', variety: '', basePrice: 0 },
largeSizePrice: 0
}
},
created() {
this.info = this.getTodayInfo()
this.largeSizePrice = this.info.basePrice * 1.4
},
methods: {
getTodayInfo() {
// 実際はWebAPIで情報を取得するが、話の単純化のため定数を返す
return { prefecture: '青森', variety: 'ふじ', basePrice: 300 }
},
onClick() {
this.$refs.email.sendText()
},
sendEmail(address) {
// 実際はWebAPIでサーバにアドレスを送信するが、話の単純化のため警告を出すだけにしている
alert(`${address}にメールを送信しました`)
}
}
}
</script>
<template>
<input v-model="text" :placeholder="placeholder" />
</template>
<script>
export default {
props: {
placeholder: { type: String, default: '' }
},
data() {
return {
text: ''
}
},
methods: {
sendText() {
this.$emit('send-text', this.text)
}
}
}
</script>
<template>
<button :class="btnClass" @click="$emit('click')">
<slot />
</button>
</template>
<script>
export default {
props: {
isSmall: { type: Boolean },
isLarge: { type: Boolean }
},
computed: {
btnClass() {
if (this.isSmall) return 'small'
return 'large'
}
}
}
</script>
これでコードは以上だ。
正直小さなコンポーネントばかりなので、そこまで読むのは難しくないと思う。しかし、この中にも可読性を下げる要因は散りばめられており、もっと大きなコンポーネントの中では、そういった小さな要因が積み重なって扱いづらいコンポーネントになってしまう。
2021/3/5 追記
Vue 3.0系では子コンポーネント側からイベントをemit
する場合、基本的にはemits
オプションを指定する必要がある。emits
オプションを指定するとコンポーネントのインターフェースが分かりやすくなるメリットもあるが、$attrs
の仕様変更のために、イベントハンドラが誤動作するのを防ぐことができる。詳しくは下記参考リンクを参照。
4. コードの問題点と改善方法
ここから上記アプリの問題点の指摘とその修正方法を記載していく。
4-1. 繰り返しを含むtemplate
まず、手始めにApp.vue
の下記部分を見ていく。
<template>
<!-- ...略... -->
<div class="today-info_list">
<div class="today-info_item">
<span>産地</span>
{{ info.prefecture }}
</div>
<div class="today-info_item">
<span>品種</span>
{{ info.variety }}
</div>
<div class="today-info_item">
<span>Mサイズ</span>
{{ info.basePrice }}円
</div>
<div class="today-info_item">
<span>Lサイズ</span>
{{ largeSizePrice }}円
</div>
</div>
<!-- ...略... -->
</template>
繰り返しは、Vue.jsに限らず一番目に着きやすいリファクタリングすべき箇所であることが多い。サンプルでは比較的単純な構造のたった4回の繰り返しだが、これがもっと大きくなってくるとコード量が増え、可読性が落ちる。また修正が必要な際は、同様の修正を複数箇所に適用しなければならず変更がしづらい。
しかし、もっと深刻な問題は**「本当に繰り返しなのか」がこのコードを書いた本人以外には分かりづらいこと**である。何回か「繰り返し」という言葉を使用してきたが、実は一箇所だけ違う箇所がある。見つけられていなければ再度コードを見返して欲しい。答えは下の折り畳み内に記載した。
Q.繰り返しではない箇所はどこか(答えはクリックして開く)
A. 本当はそんなものはない。もし、時間をかけてじっくり探した人がいたら申し訳ないが、そういう人ほど次のリファクタリングの意図が理解できるだろう。上記したような問題を解決するために、以下のファクタリングを適用する。
- 繰り返している構造をコンポーネントとして抽出
- 繰り返し部分に
v-for
を適用する
繰り返している構造をコンポーネントとして抽出
繰り返している構造を抽出したMyItem
コンポーネントを作成する。
<template>
<div class="today-info_item">
<span>{{ title }}</span>
{{ value }}
</div>
</template>
<script>
export default {
props: {
title: { type: String, required: true },
value: { type: [String, Number], required: true }
}
}
</script>
これを使用するとApp
コンポーネントは次のようになる。
<template>
<!-- ...略... -->
<div class="today-info_list">
<MyItem title="産地" :value="info.prefecture" />
<MyItem title="品種" :value="info.variety" />
<MyItem title="Mサイズ" :value="`${info.basePrice}円`" />
<MyItem title="Lサイズ" :value="`${largeSizePrice}円`" />
</div>
<!-- ...略... -->
</template>
このようにしていれば繰り返しであることは格段に分かりやすくなる。
繰り返し部分にv-for
を適用する
さて、繰り返している構造をコンポーネントとして抽出したことで、冗長な記述は大幅に減ったが、まだ検討の余地がある。シンプルにはなったが、MyItem
タグを繰り返し記載している。v-for
ディレクティブを使用することでそのような記載をよりシンプルにすることが可能だ。
v-for
をこの部分に適用するため、次のような算出プロパティを用意する
<script>
// ...略...
export default {
// ...略...
computed: {
infoItems() {
return [
{ title: '産地', value: this.info.prefecture },
{ title: '品種', value: this.info.variety },
{ title: 'Mサイズ', value: `${this.info.basePrice}円` },
{ title: 'Lサイズ', value: `${this.largeSizePrice}円` }
]
}
},
// ...略...
}
</script>
この算出プロパティを使用してtemplateを書き換えると次のようになる。
<template>
<!-- ...略... -->
<div class="today-info_list">
<MyItem v-for="item of infoItems" :title="item.title" :value="item.value" />
</div>
<!-- ...略... -->
</template>
説明のためにv-forを適用したが、今回のケースではここまでしてしまうと少しやりすぎかもしれない。しかし、繰り返している構造をコンポーネントにした場合でも、共通の属性(例えばクラス等)を指定しないといけない場合もあり、その場合はv-forを適用することでコードの記述量が減るので有用であると思う。
4-2. セットで更新しなければいけない状態
次に検討したいのはApp
コンポーネントの次の部分である。
<script>
// ...略...
export default {
// ...略...
data() {
return {
info: { prefecture: '', variety: '', basePrice: 0 },
largeSizePrice: 0
}
},
created() {
this.info = this.getTodayInfo()
this.largeSizePrice = this.info.basePrice * 1.4
},
// ...略...
}
</script>
data
にはprefecture
、variety
、basePrice
の3つのプロパティをもつオブジェクトinfo
とlargeSizePrice
が登録されている。
このうちMサイズの料金(基本料金)を表すinfo
のbasePrice
とLサイズの料金を表すlargeSizePrice
の関係に注目したい。このアプリの要件には
- Mサイズの料金を基本料金として、 Lサイズはその1.4倍の料金とする
というものがあった。これはMサイズの料金が決まれば、Lサイズの料金は自動的に決まるということであり、それぞれが独立した状態でないことを意味する。独立していない状態をdata
として登録してしまうと後々問題を引き起こす可能性がある。例えば、情報を30分に一回更新するといった要件が必要になったとする。次のようなイメージだ。
<script>
// ...略...
export default {
// ...略...
created() {
this.info = this.getTodayInfo()
this.largeSizePrice = this.info.basePrice * 1.4
setInterval(() => {
this.updateInfo()
}, 30 * 60 * 1000)
},
methods: {
// ...略...
updateInfo() {
this.info = this.getTodayInfo()
}
}
}
</script>
このコードの問題点はすぐに分かるだろう。updateInfo()
の定義内でinfo
を更新した際にlargeSizePrice
がセットで更新されていない。そんな見落としはしないと思う人もいるかもしれないが、実際のアプリケーションの中では、エラー処理だったり、WebAPIから取得したデータの整形が行われていたりとupdateInfo()
のコードが数十行のサイズになることもある。また他の箇所でもbasePrice
が更新されるようなことがあれば、その度に忘れずlargeSizePrice
を更新しなければならない。
上記したような問題を解決するために、以下のリファクタリングを適用する。
- セットで更新しなければいけない状態は算出プロパティとして定義する
セットで更新しなければいけない状態は算出プロパティとして定義する
Vue.jsに慣れていない人は、算出プロパティを使えていないことが多々ある。この原因は憶測だが、算出プロパティがVue.js特有の機能であることや、そもそも算出プロパティを使わなくとも動くものは作れてしまうこと、あとは名前自体が何か計算が必要なときに使うものという誤解を招いていたりもしていると思う。サンプルアプリではまさに計算するために使っているが、状態に応じて決まるclassや、アイコンのパス指定などにも利用できる。
さて、話が脱線してしまったが、修正自体はすごくシンプルだ。
<script>
// ...略...
export default {
// ...略...
data() {
return {
info: { prefecture: '', variety: '', basePrice: 0 }
}
},
computed: {
infoItems() {/* ...略... */},
largeSizePrice() {
return this.info.basePrice * 1.4
}
},
created() {
this.info = this.getTodayInfo()
},
// ...略...
}
</script>
これでbasePrice
が更新されたときは自動的にlargeSizePrice
が再計算される。
4-3. 所属がおかしな状態
次にMyButton
コンポーネントを検討する。上で記載したコードを再掲する。
<template>
<input v-model="text" :placeholder="placeholder" />
</template>
<script>
export default {
props: {
placeholder: { type: String, default: '' }
},
data() {
return {
text: ''
}
},
methods: {
sendText() {
this.$emit('send-text', this.text)
}
}
}
</script>
このコンポーネントのメソッドsendText()
は親コンポーネント(MyTextBox
を使う側、すなわちApp
コンポーネント)で使用されることを想定して定義されている。
App
コンポーネントを見てみるとtemplateのMyTextBox
の呼び出し箇所でref
属性が指定されており、onClick
メソッド内でsendText()
を呼び出していることが分かる。
<template>
<!-- ...略... -->
<MyTextBox ref="email" placeholder="メールアドレス" @send-text="sendEmail" />
<MyButton isSmall @click="onClick">
抽選する
</MyButton>
<!-- ...略... -->
</template>
<script>
// ...略...
export default {
// ...略...
methods: {
getTodayInfo() {/* 省略 */},
onClick() {
this.$refs.email.sendText()
},
sendEmail(address) {/* 省略 */}
}
}
</script>
このようなことをしてしまうと、コンポーネント同士は密結合になり、コンポーネントを超えた影響を考慮しなければならなくなってしまう。クラスのアクセス修飾子のように外部からのアクセスを制限できる仕組みがあれば良いが、恐らくVue.jsのコンポーネントにはそのような仕組みはないので、そもそも外部のコンポーネントの状態へのアクセスやメソッドの使用をしないように注意する他なさそうだ。
上記した問題を解決するために、以下のリファクタリングを適用する
- 適切なコンポーネントに状態を移動
適切なコンポーネントに状態を移動
そもそも親コンポーネントで使用するメソッドであれば、親コンポーネント内で定義すれば良いのだが、そうなっていないのは入力フォームの状態をMyTextBox
側で持ってしまっていることに原因がある。そこで、親コンポーネント側から入力フォームの状態を受け取るようにし、変更があれば親コンポーネントにイベント送信して通知するように修正する。
<template>
<input :value="text" :placeholder="placeholder" @input="onInput" />
</template>
<script>
export default {
props: {
text: { type: String, required: true },
placeholder: { type: String, default: '' }
},
methods: {
onInput(event) {
this.$emit('input', event.target.value)
}
}
}
</script>
App
コンポーネントでは入力フォームの状態を定義し、MyTextBox
にprops
として渡し、変更イベントを受け取り次第状態を更新するようにする。
<template>
<!-- ...略... -->
<div class="form">
<MyTextBox
:text="address"
placeholder="メールアドレス"
@input="onInput"
/>
<MyButton isSmall @click="onClick">
抽選する
</MyButton>
</div>
</div>
</template>
<script>
// ...略...
export default {
name: 'app',
components: {/* ...略... */},
data() {
return {
info: {/* ...略... */},
address: '' // <-- 入力フォーム用の状態を追加
}
},
computed: {/* ...略... */},
created() {/* ...略... */},
methods: {
getTodayInfo() {/* ...略... */},
onClick() {
alert(`${this.address}にメールを送信しました`) // <-- ボタンクリック時に子コンポーネントは介さないように修正
},
onInput(text) {
this.address = text // <-- 入力フォームに変更があれば、状態を変更
}
}
}
</script>
ところで、なぜこのアプリの実装者は最初にMyTextBox
の方に入力フォームの状態を定義したのだろうか。考えられる理由としては**App
コンポーネント側に状態を増やしたくなかったから**ということがあげられる。
今回の例では分かりづらいかもしれないが、複数のコンポーネント間でデータの共有をしようと思うとどうしても上位のコンポーネント側に状態が集まってしまいがちで、それを解決するために子側に状態を持たせてしまうことがある。しかし、コードの見かけ上状態が減ったように見えても、その状態が必要であることには変わりはない。コンポーネントの責務を分離するためにも、原則、状態は適切なコンポーネントに配置すべきだと思う。コンポーネントで使う状態が増えすぎてしまうような場合にはVuex
等の状態管理ライブラリを導入するのも手だ。
さて、もっと簡潔な記載にするためMyTextBox
にv-model
を適用したいと思うかもしれない。
<MyTextBox v-model="address" placeholder="メールアドレス" />
カスタムコンポーネントではv-model
を使用する場合、Vue 2.xのデフォルトではvalue
をプロパティとして渡し、input
イベントによって値を更新するようになっているので、text
をプロパティとして渡したい場合は下記のような修正が必要だ。
<template>
<input :value="text" :placeholder="placeholder" @input="onInput" />
</template>
<script>
export default {
model: { prop: 'text' }, // <-- これを追記
props: {
text: {/* ...略 ... */},
placeholder: {/* ...略... */}
},
methods: {/* ...略... */}
}
</script>
2021/3/15 追記
Vue 3.0系ではデフォルトではmodelValue
をプロパティとして渡し、update:modelValue
イベントによって値を更新するようになっている。コンポーネントのmodel
オプションは廃止されたので、text
をプロパティとして渡したい場合はv-model
の引数として指定する。
<template>
<MyTextBox v-model:text="address" placeholder="メールアドレス" />
</template>
また、onInput
メソッドでemit
するイベントはupdate:text
とする必要がある。
4-4. 矛盾しうるプロパティ
最後に見ていくのはMyButton
コンポーネントのプロパティ部分だ。
<template>
<!-- ...略... -->
</template>
<script>
export default {
props: {
isSmall: { type: Boolean },
isLarge: { type: Boolean }
},
computed: {/* ...略 */}
}
指定できるのは、サイズ変更のための属性である。真偽値のプロパティは、コンポーネントを使う側では<MyButton isLarge>クリック</MyButton>
のように値なしで属性を記載するだけで、:isLarge="true"
と指定したと見なされるため、若干使い勝手が良いという側面がある。
一方で、<MyButton isSmall isLarge>クリック</MyButton>
のように意味が矛盾するような指定もできてしまうので注意が必要だ。それ自体が直接問題を引き起こすようなことは少ないかもしれないが、デフォルト値の指定がしづらかったり、両方指定された場合の適用優先度を把握しなければならなかったりすることで、可読性が落ちる可能性がある。
上記問題を解決するために、以下のリファクタリングを適用する
- 同種のプロパティをまとめる
同種のプロパティをまとめる
isSmall
とisLarge
がともにサイズに関連するプロパティであったがために、矛盾が生じる指定ができてしまっていたので、これらをサイズを指定するためのプロパティにまとめてしまえば良い。
<template>
<!-- ...略... -->
</template>
<script>
export default {
props: {
size: {
type: String,
default: 'small',
validator: val => ['small', 'large'].includes(val)
}
},
computed: {
btnClass() {
if (this.size === 'small') return 'small'
return 'large'
}
}
}
</script>
validator
の記述は若干冗長だが、size
というプロパティがどのような値を想定しているかのドキュメント代わりにもなるので、あった方が良いだろう。
参考程度に載せておくが、コンポーネントライブラリのVeutifyのv-btnコンポーネントは、まさにこのリファクタリングを適用する前のようなプロパティ指定でサイズ変更できるようになっている。
<v-btn large>クリック</v-btn>
他のリファクタリングでも言えることだが、必ずしもこれが正解というものはなく、メリット・デメリットを考慮した上でどうすべきかという判断が必要であることは注意していただきたい。
4-5. リファクタリング後のコード
これで、適用したいリファクタリングは全て行った。改めてリファクタリング後のコードを記載すると次のようになる。
<template>
<div id="app">
<h1>Ringo Juice</h1>
<div class="today-info">
<h4 class="today-info_title">本日のりんご</h4>
<div class="today-info_list">
<MyItem v-for="item of infoItems" :title="item.title" :value="item.value" />
</div>
</div>
<p class="message">
りんごジュースは1日100名様限定です。<br />当選者のみが購入可能です。<br />
下記フォームにメールアドレスを入力してください。<br />抽選結果が届きます。
</p>
<div class="form">
<MyTextBox v-model="address" placeholder="メールアドレス" />
<MyButton size="small" @click="onClick">
抽選する
</MyButton>
</div>
</div>
</template>
<script>
import MyTextBox from '@/components/MyTextBox.vue'
import MyButton from '@/components/MyButton.vue'
import MyItem from '@/components/MyItem.vue'
export default {
name: 'app',
components: { MyTextBox, MyButton, MyItem },
data() {
return {
info: { prefecture: '', variety: '', basePrice: 0 },
address: ''
}
},
computed: {
infoItems() {
return [
{ title: '産地', value: this.info.prefecture },
{ title: '品種', value: this.info.variety },
{ title: 'Mサイズ', value: `${this.info.basePrice}円` },
{ title: 'Lサイズ', value: `${this.largeSizePrice}円` }
]
},
largeSizePrice() {
return this.info.basePrice * 1.4
}
},
created() {
this.info = this.getTodayInfo()
},
methods: {
getTodayInfo() {
return { prefecture: '青森', variety: 'ふじ', basePrice: 300 }
},
onClick() {
alert(`${this.address}にメールを送信しました`)
}
}
}
</script>
<template>
<div class="today-info_item">
<span>{{ title }}</span>
{{ value }}
</div>
</template>
<script>
export default {
props: {
title: { type: String, required: true },
value: { type: [String, Number], required: true }
}
}
</script>
<template>
<input :value="text" :placeholder="placeholder" @input="onInput" />
</template>
<script>
export default {
model: { prop: 'text' },
props: {
text: { type: String, required: true },
placeholder: { type: String, default: '' }
},
methods: {
onInput(event) {
this.$emit('input', event.target.value)
}
}
}
</script>
<template>
<button :class="btnClass" @click="$emit('click')">
<slot />
</button>
</template>
<script>
export default {
props: {
size: {
type: String,
default: 'small',
validator: val => ['small', 'large'].includes(val)
}
},
computed: {
btnClass() {
if (this.size === 'small') return 'small'
return 'large'
}
}
}
</script>
さて、リファクタリング前と比べていかがだろうか。正直、劇的な変化はないのでがっかりした方もいるかもしれない。しかし、今回紹介したような問題のある書き方を避け、避けられなくとも問題点を理解した上で、コンポーネントを書いていくことができれば、より大規模なアプリを開発する上で役に立つ場面があると信じている。
5. まとめ
今回見てきた、初心者がやりがちだと思うコードの書き方の問題点とその解決方法を改めてまとめる。
繰り返しを含むtemplate
- 問題点
- コード量が単純に増え可読性が落ちる
- 同様の修正が複数箇所で必要になり、変更しづらい
- そもそも繰り返しなのかが、書いた本人以外に分かりづらい
- 解決方法
- 繰り返しの構造をコンポーネントとして抽出する
- 繰り返している箇所に
v-for
を適用する
セットで更新しなければいけない状態
- 問題点
- セットで更新しなければならないことを忘れてしまいがち
- 解決方法
- セットで更新しなければいけない状態は算出プロパティとして定義する
所属がおかしな状態
- 問題点
- コンポーネント同士が密結合になり、コンポーネント外の影響を考慮しなければならなくなる
- 解決方法
- 適切なコンポーネントに状態を移動する
- (場合によっては、Vuex等状態管理ライブラリの使用を検討する)
矛盾しうるプロパティ
- 問題点
- デフォルト値の指定がしづらい
- 矛盾する指定をしたときに、どちらが優先して適用されるか分かりづらい
- 解決方法
- 同種のプロパティをまとめる
6. あとがき
局所的に問題点の指摘やその解決方法は記載できたかと思うが、いかに扱いやすいコンポーネントになったかという説明ができなかったので、時間があれば補足したい。特にコンポーネントのテストはしやすくなっているはずで、そういった説明も加えられると良いかなと思う。