はじめに
こんにちは、Gakken LEAPのフロントエンドエンジニアのkouです。
今回は、ShikakuPassプロジェクト(Vue 3、TypeScript、Nuxt 3、Tailwind CSSを使用)で行ったwatch
からcomputed
へのリファクタリング実践についてご紹介します。
注意:本記事のコード例は技術概念の説明のためのサンプルコードであり、実際の業務コードではありません。
フォーム機能では、以下のような複数の状態を扱う必要があります:
- ユーザーデータの有無
- フォーム入力の完了状態
- バリデーションエラーの有無
複数の状態を扱うフォーム機能の実装において、当初はwatch
を使って変更を監視していましたが、処理箇所が増えるにつれ状態管理が煩雑になり、保守性が課題となってきました。そこで最終的にcomputed
を活用した実装にリファクタリングすることにしました。
watchとcomputedの基本的な使い方
リファクタリングプロセスについて詳しく説明する前に、まずVueのwatch
とcomputed
の基本的な使い方と違いを理解しましょう。
watchとは?
watch
は、特定のリアクティブな値の変化を監視し、それに応じて副作用を実行する際に使用します。対象の値が変更されると、対応するコールバック関数が呼び出されます。
watch([username, email, password, confirmPassword], () => {
isFormCompleted.value = !!(username.value && email.value && password.value && confirmPassword.value);
});
computedとは?
computed
は、複数のリアクティブな値から導出される派生状態を定義するために使います。依存する値が変更されたときのみ再計算され、結果はキャッシュされます。
const isFormCompleted = computed(() => {
return !!(username.value && email.value && password.value && confirmPassword.value);
});
実際の問題とリファクタリングの動機
フォーム機能において、isFormCompleted
状態は複数のシナリオで更新される必要がありました:
- ユーザーがフォームフィールドを入力する時
- ユーザーが操作をキャンセルする時
- データの初期ロード時
- ユーザーが「編集」ボタンをクリックする時
watch
を使った実装では、これらのそれぞれの場面で isFormCompleted.value
を明示的に書き換える必要があり、次のようなコードが存在していました:
// フォーム入力の監視
watch([username, email, password, confirmPassword], () => {
isFormCompleted.value = !!(username.value && email.value && password.value && confirmPassword.value);
});
// キャンセル時の処理
const handleCancel = () => {
isEditing.value = false;
isFormCompleted.value = !!userData.value;
};
// データ変更時の処理
const handleEdit = () => {
isFormCompleted.value = false;
isEditing.value = true;
};
// 初期化時
if (userData.value) {
isFormCompleted.value = !isError.value;
}
このアプローチの問題点は以下のとおりです:
- 状態更新のロジックが各所に分散し、追跡・保守が困難
- 更新漏れや整合性の欠如が発生しやすい
- DRY原則に反し、コードの重複が多い
- 状態の更新条件が明確でなく、全体像の把握が難しい
computedプロパティへのリファクタリング
上記の課題を解決するために、状態の算出ロジックを computed
に集約しました。以下のような補助的な算出プロパティを用意したうえで:
const hasUserData = computed(() => !!userData.value);
const hasValidFormFields = computed(
() => !!(username.value && email.value && password.value && confirmPassword.value)
);
最終的な isFormCompleted
のロジックは次のように集約されました:
const isFormCompleted = computed(() => {
// ユーザーが新しい情報を入力中(既存データの変更または初回入力)
if (isEditing.value || !hasUserData.value) {
// すべてのフィールドが入力済みでエラーがないかをチェック
return hasValidFormFields.value && !isError.value;
}
// ユーザーが既存のデータを持ち、変更していない場合、データの存在とエラーの有無のみをチェック
return hasUserData.value && !isError.value;
});
これにより、状態の更新はリアクティブに行われるため、明示的な更新処理は不要になり、以下のように処理関数も簡潔になりました:
const handleCancel = () => {
isEditing.value = false;
};
const handleEdit = () => {
isEditing.value = true;
};
なぜcomputedを選択するのか
このリファクタリングでは以下のような利点が得られました:
- 自動反映:依存関係が変わると自動で再評価される
- パフォーマンス最適化:キャッシュにより不要な再計算を防ぐ
- ロジックの一元化:状態計算を一箇所に集約できる
- 宣言的な記述:意図が明確で、Vueらしい設計になる
特に重要なのは、computed
の価値は単なるキャッシュにとどまらず、明確な依存グラフの構築とリアクティブな再評価の最適化にあります。依存するデータが変化したときだけ必要な箇所が再レンダリングされるという点で、アプリケーション全体のパフォーマンスにも寄与します。
リファクタリングの成果と収穫
今回の変更によって、以下のような成果が得られました:
- コードが簡潔に:重複や条件分岐の整理により行数が減少
- ロジックが明確に:状態計算が一箇所に集約されて見通しがよくなった
- 保守性が向上:変更時に複数箇所を意識する必要がなくなった
このリファクタリング体験により、Vueアプリケーションでcomputed
プロパティを適切に使用することの重要性を深く認識しました。派生状態に対しては、computed
は通常watch
よりも適しており、コードをより宣言的で理解しやすく、保守しやすくします。もちろん、watch
は副作用(API呼び出し、DOM操作など)を実行する必要がある場合にはまだ有用ですが、純粋な状態計算に対しては、computed
がより良い選択です。
まとめ
今回の watch
から computed
へのリファクタリングは、コードの可読性・保守性を大きく改善し、Vueのリアクティブ性への理解も深まりました。
今後も、機能や状態の性質に応じて適切なリアクティブAPIを選択しながら、より良いプロダクトづくりを進めていきたいと思います。
エンジニア募集中
Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています!!
ぜひお気軽にカジュアル面談へお越しください!!