😊

Vue Composableで状態をカプセル化してVueコンポーネントを整理する

2023/03/06に公開

公式ドキュメントによるコンポーザブル | Vue.jsの定義は、以下です。

Vue アプリケーションの文脈で「コンポーザブル(composable)」とは、Vue の Composition API を活用して状態を持つロジックをカプセル化して再利用するための関数です。

再利用に一番焦点が当たりますが、コード整理のためにも使えます。

コンポーザブル | Vue.js

コンポーザブルは再利用だけでなくコード整理のために抽出することもできます。

コード整理の一種として、Composableでカプセル化をしました。

と、いっても、状態をcomposableに切り出すと自動的にカプセル化されます。

カプセル化

以下参照
カプセル化とは - 意味をわかりやすく - IT用語辞典 e-Words

コンポーザブル前

1コンポーネント内にanimalとvegetableを登録できるフォーム同時に作る例を仮定します。

コードはこうです。

src/components/Page.vue

<template>
  <div>
    <hr>
    <div>
      <form @submit.prevent="onSubmitAnimalName">
        <div>
          <label for="name">ANIMAL NAME</label>
          <input type="text" v-model="animalNameValue" />
        </div>
        <button type="submit">Submit</button>
      </form>
      <p v-for="animal in animals" :key="animal.id">
        {{ animal.name }}
      </p>
    </div>
    <hr>
    <div>
      <form @submit.prevent="onSubmitVegetableName">
        <div>
          <label for="name">VEGETABLE NAME</label>
          <input type="text" v-model="vegetableNameValue" />
        </div>
        <button type="submit">Submit</button>
      </form>
      <p v-for="vegetable in vegetables" :key="vegetable.id">
        {{ vegetable.name }}
      </p>
    </div>
  </div>
</template>
<script setup>
import { ref } from "vue";

const animals = ref([]);
const animalNameValue = ref("");
const animalId = ref(0);
const onSubmitAnimalName = () => {
  animals.value.push({id: animalId.value, name: animalNameValue.value});
  animalNameValue.value = "";
  animalId.value = animalId.value + 1;
};

const vegetables = ref([]);
const vegetableNameValue = ref("");
const vegetableId = ref(0);
const onSubmitVegetableName = () => {
  vegetables.value.push({id: vegetableId.value, name: vegetableNameValue.value});
  vegetableNameValue.value = "";
  vegetableId.value = vegetableId.value + 1;
};
</script>

animal nameを入力すると、animal nameが表示され、vegetable nameを入力すると vegetable nameが画面に表示されるフォームです。

課題

前述のコードの課題は下記の2つです。

  • 同一コンポーネントでanimalとvegetableの状態が存在し、それぞれから他方の状態(animalsvegetables)を直接書き換える事ができる
  • animalsもしくはvegetablesの更新のためのみに使われる内部状態 animalIdvegetableIdが露出しているのでよみづらい
  • 名前追加のロジックを保証するために、comonentをマウントしてテストを書くのが面倒。

課題整理の方針

このコードを整理するには以下の3つの方法が考えられます。

  • componentを共通化しpropsで使い分ける
  • componentを分ける
  • → composableによるカプセル化を

今回は矢印で示した第3の選択肢でコードを整理します。

(今回のようなシンプルな事例だと、「componentを共通化しpropsで使い分ける」や「componentを分ける」のが一番ですが、主題外なので第3の選択肢をとります)

コンポーザブル利用後

整理した結果は以下です。

なお、templateには変更がないので表記を省略しました。また、animalとvegetableでlogicに差異がないので主にanimalのみを表記しました。

src/components/Page.vue

<script setup>
import usePageAnimal from "@/components/composables/usePageAnimal";
import usePageVegetable from "@/components/composables/usePageVegetable";

const { animals, animalNameValue, submitAnimalName } = usePageAnimal();
const onSubmitAnimalName = () => {
  submitAnimalName();
};

const { vegetables, vegetableNameValue, submitVegetableName } = usePageVegetable();
const onSubmitVegetableName = () => {
  submitVegetableName();
};
</script>

src/components/composables/usePageAnimal.js

import { computed, ref } from "vue";

export default function usePageAnimal() {
  const animals = ref([]);
  const animalNameValue = ref("");
  const animalId = ref(0);
  const submitAnimalName = () => {
    animals.value.push({
      id: animalId.value,
      name: animalNameValue.value,
    });
    animalNameValue.value = "";
    animalId.value = animalId.value + 1;
  };
  return {
    animals: computed(() => animals.value),
    animalNameValue,
    submitAnimalName,
  };
}

解説

やったことは、ほぼ、状態をcomposableにまるっと移動しただけです。

これにより以下の課題を解決できました。

同一コンポーネントでanimalとvegetableの状態が存在し、それぞれから他方の状態(animalsvegetables)を直接書き換える事ができる

Page.vueから状態が全て別ファイルにカプセル化しました。

animalsは、 animals: computed(() => animals.value),とするとこでカプセル化(直接変更不可とし、変更する場合は特定のメソッド経由(今回の事例では、submitAnimalName)に限る)ことができました。

animalsもしくはvegetablesの更新のためのみに使われる内部状態 animalIdvegetableIdが露出しているのでよみづらい

Page.vueでひつようとされる状態もしくは関数のみをimportしたところ、animalIdをPage.vueから隠すことができました。

const { animals, animalNameValue, submitAnimalName } = usePageAnimal();

名前追加のロジックを保証するために、comonentをマウントしてテストを書くのが面倒。

javascriptファイルに切り出すことができましたので、vue-test-utils | Vue Test Utilsを必要とせずに、テストを書く事ができます。

下記はjest例です。

import usePageAnimal from "@/components/composables/usePageAnimal";

describe('usePageAnimal', () => {
  it('submitAnimalName', () => {
    const { animals, animalNameValue, submitAnimalName } = usePageAnimal();
    animalNameValue.value = 'usagi';
    expect(animals.value.length).toBe(0);
    submitAnimalName();
    expect(animals.value.length).toBe(1);
    expect(animals.value[0].name).toBe('usagi');
  })
})

所感

関数の共通化だけでなく、コードの整理でもcomposable便利です。

Discussion