27
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NRI OpenStandiaAdvent Calendar 2024

Day 2

[Vue]あなたが良いコードを書くのに必要なたった二つの武器

Last updated at Posted at 2024-12-01

はじめに

この記事では、

良いコード = リアクティブなコード

と定義しています。

主にVue 3を前提としたコード提示をするので、コード部分についてはVueの基本文法を知っている人がメインターゲットです。
ただし、似たような話は他のSPAフレームワーク(筆者の感覚ではSvelteも結構近いはず)にも言えるので、モダンフレームワークを触る人に通じる話かと思います。

もしより良い書き方や間違いの指摘があればどしどしコメントいただけますと幸いです。

:skull: リアクティブでないコードとは

筆者が入っているプロジェクト(Vue 3)において、下記の要件を満たすべき画面がありました(この記事のためにかなり簡単にしています)。

  • 複数のチェックボックスが並んでいる
  • すべてのチェックボックスを選択するとボタンが押せる

みなさんならどう書くでしょうか?
このコードのレビューをお願いしたいと依頼されて筆者が出会ったコードは下記のようなものでした(大体の内容はそこから引用していますが、コード自体は生成AIをベースに作りました)。

<script setup lang="ts">
import { ref } from 'vue';

type CheckboxItem = {
  id: number;
  label: string;
  checked: boolean;
};

// チェックボックスの項目データ
const items = ref<CheckboxItem[]>([
  { id: 1, label: '選択肢1', checked: false },
  { id: 2, label: '選択肢2', checked: false },
  { id: 3, label: '選択肢3', checked: false },
  { id: 4, label: '選択肢4', checked: false },
  { id: 5, label: '選択肢5', checked: false },
]);

const areAllChecked = ref(false);

// チェック状態が変更された時の処理
const updateCheckedStatus = (): void => {
  areAllChecked.value = items.value.every((item) => item.checked);
};


</script>

<template>
  <div class="checkbox-container">
    <h2>チェックボックス一覧</h2>

    <div v-for="(item, index) in items" :key="index" class="checkbox-item">
      <input
        type="checkbox"
        :id="'checkbox-' + index"
        v-model="items[index].checked"
        @change="updateCheckedStatus"
      />
      <label :for="'checkbox-' + index">{{ item.label }}</label>
    </div>

    <button type="button" :disabled="!areAllChecked">Button</button>
  </div>
</template>
  • 選択肢を配列として、v-forでループで表示する
  • ボタンの押下可否は:disabledで変数を渡す

このあたりはよさそうです。シンプルかつVueの機能を活用できていると言えるでしょう。ではこのコードは問題ないと言えるでしょうか?

残念ながらこのコードはリアクティブではなく、Vueを活かしきれていません。何がいけないのでしょうか?

:mortar_board: そもそもリアクティブって?

そもそも「リアクティブ」とはどういう意味でしょうか?辞書を引くと、

  • よく反応する
  • 反応の早い
  • 反応性の高い
  • 敏感な

という言葉が見られます(参考:英辞郎

VueなどのSPAフレームワークにおけるリアクティブとは、「ある値(状態)が変化したときに、その変化に反応する仕組み」と言えます。例えばrefによる変数の宣言がそれにあたります。Vueでは、単に変数を宣言するときと「リアクティブな変数」を宣言するときとで記法が異なります。

const foo = 0; // 普通の変数
const bar = ref(0); // リアクティブな変数

barはリアクティブな変数ですが、具体的にどんなことが実現できるのでしょうか?
最も単純な例は、「Ref変数が変化するとそれに応じて見た目も変わる」です。
例えば、以下のような<template>があったとします。

<template>
  <p>{{ bar }}</p>
</template>

単にbarの値を出力するだけの要素ですが、何らかの処理でbarに変更が加わると、<p>タグの中身はそれに応じて勝手に更新されます!

<script setup lang="ts">
const bar = ref(0); // リアクティブな変数
const increment = () => {
  bar.value += 1;
}
</script>

<template>
  <button type="button" @click="increment">Inc</button>
  <!-- ↑のbuttonを押すたびに↓の値が増える -->
  <p>{{ bar }}</p>
</template>

この「勝手に更新」される仕組みがリアクティブです。Vueでは、<template>から<script>内のリアクティブな値を参照していた場合、そのリアクティブな値に変化があるたびに影響を受ける部分が自動的に更新されます。

筆者はこの関係をよく「依存関係」と表現します。ある値Aが他の値Bに依存しているとき、Bの変化に合わせて良しなにAも勝手に変化してくれることが望ましいです。逆にいえば、Bに変化を加えるたびに、自分でわざわざAへの操作も併せて行うやり方は、変更漏れの危険性がありますし、何よりAがどのようなロジックで決定されるものなのかがはっきりしません。すなわち、依存関係が可視化できず、かつ、保証しにくいのです。

:spy: リアクティブだと何がうれしいのか?

前項の最後に書いた、「依存関係の可視化と保証」がリアクティビティがもたらす最強の恩恵と筆者は考えています。変数や要素がどのように依存しあっているのかを「宣言的」に記述することで、コードの可読性を高め、かつ変更漏れを防ぐことができるのです。

変数によって要素を反応させる方法はVueの記法としていくつか存在します。先ほどの{{ }}を使う方法もありますし、v-ifで表示/非表示を切り替える、v-forでイテレーティブに表示する、などもそれにあたります。

もう少し高度なことはできないでしょうか?例えば、変数→要素だけでなく、変数→変数、すなわち他の変数に応じて値が決まる変数、はどのように定義するのでしょうか?

Vueにはそれに応えるcomputedというものがあります。例えば、

  • 税抜き価格(exTax
  • 税率(ratio

の二つの可変な値から、自動的に税込み価格(inTax)を算出したいとします。

この場合、inTaxexTaxratioに依存しています。こういうときにcomputedが活躍します。
結論として、以下のように定義できます。computedの中身は関数を書く決まりになっています。

const exTax = ref(1000);
const ratio = ref(0.1);

const inTax = computed(() => exTax.value * ratio.value);

inTaxが何を表現しているのか一目瞭然です。明らかに、exTaxratioを乗じたものと分かります。これが「可視化」です。inTaxの宣言箇所を見れば、「inTaxがどのように決まるのか」がロジックとして書かれています。
computedのすごいところは、その値が変化すれば、上記の例でいえば「exTaxratioに変更が加われば」、自動的にinTaxが再計算されるところです。これが「保証」です。依存先が変われば依存元も追従する仕組みをVueが提供してくれるので、開発者はexTaxratioに変更を加えるロジックのみ書けばよいのです。

もしVueが提供するリアクティビティを活用しなければどうなってしまうのでしょうか?
同じような内容をあえてcomputedを使わないで書くと、次のようになります(<template>がリアクティブに変更されることは利用するのでref自体は残します)。

const exTax = ref(1000);
const ratio = ref(0.1);

 // 初期値の定義はされているが、この値そのものが何を表現するのか不明
const inTax = ref(exTax.value * ratio.value);

// exTax更新用関数
const updateExTax = (value: number) => {
  exTax.value = value;
  inTax.value = exTax.value * ratio.value; // 忘れるな!
}

// ratio更新用関数
const updateRatio = (value: number) => {
  ratio.value = value;
  inTax.value = exTax.value * ratio.value; // 忘れるな!
}

どうでしょうか?記述量が増えた割にメリットは感じられるでしょうか?
筆者はこの書き方を「setter的な書き方」を表現することがあります。能動的にinTaxへの代入(set)をしなければinTaxを正しく扱えない方式を指し、基本的にVueにおけるアンチパターンと考えています。

:writing_hand: 1つ目の武器: computed

では答え合わせといきましょう。先ほどのコードのどこがダメだったのでしょうか?
勘のいいひとはお分かりだと思いますが、以下の部分です。

const areAllChecked = ref(false);

// チェック状態が変更された時の処理
const updateCheckedStatus = (): void => {
  areAllChecked.value = items.value.every((item) => item.checked);
};

コードを読む限り、areAllCheckedは「itemsのすべての要素のcheckedtrueである」かどうかを表す値です。このような意図があるにも関わらず、areAllCheckedは宣言時は単にref(false)となっている点で、このロジックが可視化できていません。また、itemsが更新されたときに、依存関係にあるareAllCheckedを自力で更新しようとしなければならず、手間が増えています。

改善後は以下の通りです。

const areAllChecked = computed(() => items.value.every((item) => item.checked));

areAllCheckedの宣言時にcomputedを使って宣言することで、簡潔にリアクティビティを持たせています。

:ok_hand: こういうケースはどうする?

別の例を見てみましょう。

<script setup lang="ts">
import { ref } from 'vue'

interface UserProfile {
  name: string
  theme: 'light' | 'dark'
}

const userProfile = ref<UserProfile>({
  name: 'John',
  theme: 'light'
})

const api = {
  async updateUser(profile: UserProfile): Promise<void> {
    // ユーザ情報を更新するAPIを呼び出す
  }
}

// プロフィール名更新
const updateName = async () => {
  userProfile.value.name = 'Jane'
  await api.updateUser(userProfile.value)
}

// テーマ切り替え
const toggleTheme = async () => {
  userProfile.value.theme = userProfile.value.theme === 'light' ? 'dark' : 'light'
  await api.updateUser(userProfile.value)
}
</script>

<template>
  <button @click="updateName">Update Name</button>
  <button @click="toggleTheme">Toggle Theme</button>
  <pre>{{ userProfile }}</pre>
</template>

※本質に注目するため、できる限りコード量はそぎ落としています。

userProfileというオブジェクトがあり、このオブジェクトに対する変更が加わる度にそれを永続化する仕組みを実装したいです(具体的にはAPIを呼び出す)。変更はupdateNametoggleThemeで行われるので、それぞれの関数の中に更新処理を埋め込んでいます。こうすれば、変更に応じて特定の関数を実行できます。目的は達成していますね。

しかし、このコードもイマイチです。もう一つ上のステージを目指してみましょう。何が足りないのでしょうか?

:pray:「因果関係」を正しく表現したい

今回は最初のケースとは異なります。なぜなら登場する変数がuserProfileの一つだけで、いわゆる「依存関係」に該当する変数間の関係性がありません。このようなケースでもリアクティブは関係あるのでしょうか?

このコンポーネントで実現したいことは、「userProfileが変更されるたびに更新関数を実行する」のはずです。ところが、このコードでは「userProfileを変更する関数の中で更新関数を実行する」を実現しています。
これらは微妙に違います。前者はuserProfileの変化に反応することを明記しています。一方、後者は変化を引き起こす根源の事象に着目しています。すなわち、「userProfileに変更を与えうる処理をすべて把握し、それぞれの箇所に更新関数を埋め込もう」という発想です。

こう書き表すと、前章の「setter的な書き方」に近いアンチパターンと分かりやすくなります。更新関数はuserProfileの変化に反応して実行されてほしいのに、その「因果関係」が表現できていません。このようなケースではどうしたらよいでしょうか?

:eye: 2つ目の武器: watch

ここで登場するのがwatchになります。watchの第一引数にリアクティブな変数を渡し、第二引数に関数を渡すと、変数の変化に応じてリアクティブに関数を実行してくれます。

<script setup lang="ts">
import { ref, watch } from 'vue';

// リアクティブな変数
const counter = ref(0);

// 変数の変更を監視して処理を実行
watch(counter, (newValue, oldValue) => {
  console.log(`Counter changed from ${oldValue} to ${newValue}`);
});
</script>

<template>
  <button @click="counter++">Increment Counter</button>
  <p>Counter: {{ counter }}</p>
</template>

これは、変数の変化→処理の実行という因果関係を表現するのに最適です。因果関係を記述できるので、コードの可読性も上がり、何より実装漏れの心配がありません。

先の例をwatchを使って実装するとこのようになります。

<script setup lang="ts">
import { ref, watch } from 'vue';

interface UserProfile {
  name: string;
  theme: 'light' | 'dark';
}

const userProfile = ref<UserProfile>({
  name: 'John',
  theme: 'light',
});

const api = {
  async updateUser(profile: UserProfile): Promise<void> {
    // ユーザ情報を更新するAPIを呼び出す
  },
};

// プロフィール名更新
const updateName = async () => {
  userProfile.value.name = 'Jane'
}

// テーマ切り替え
const toggleTheme = async () => {
  userProfile.value.theme = userProfile.value.theme === 'light' ? 'dark' : 'light'
}

// watchを使って変更を検知し、APIを呼び出す
watch(userProfile, async (newProfile) => {
  await api.updateUser(newProfile);
});
</script>

<template>
  <button @click="updateName">Update Name</button>
  <button @click="toggleTheme">Toggle Theme</button>
  <pre>{{ userProfile }}</pre>
</template>

watchを使うことで、userProfileの変更に起因して何が起きるのかを記述することができました。繰り返しになりますが、因果関係をはっきりとさせることで、どの変数の変更がどのような副作用を及ぼすのかを追跡しやすくなります。違う言い方をすれば、変数の副作用の抜け漏れを防ぐことができます。

:point_up: リアクティブを使いこなしてこそVueを使う意味がある

いかがでしょうか。computedwatchというシンプルかつたった2つのVueの武器を紹介しましたが、その強力さが少しでも伝われば嬉しいです。
最後に、computedwatchの主な使い分けをまとめます。

computedを使うといいケース

  • ある変数(複数可)に依存して決まる変数を定義したい場合
    • 例:配列から条件に合う要素だけ抜き出す
    • 例:変数同士で計算を行う

watchを使うといいケース

  • ある変数の変化をきっかけに何かをトリガーしたい場合
    • 例:値が変わるたびにAPIを呼び出す
    • 例:値が変わるたびにDOM要素を変更したい

筆者は、Vueを使う最大のメリットは「シンプルにリアクティビティを実現できること」だと考えています。ぜひこのメリットを活かしたSPA開発を進めていきましょう!

27
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?