🔧

Vue2のプロジェクトにCompositionAPIを導入してリファクタリングしてみる

2021/06/15に公開

Vue3で導入されたCompositionAPIを使うとReactで言うところのHooksのようにロジックを分離しやすくなるのですが、周辺ライブラリがまだ対応できていないものも多かったりして、まだ気軽に移行しにくいと感じている方も多いかと思います。

そこで今回はVue2のプロジェクトにCompositionAPIを導入してみることにします。

プロジェクトを用意する

今回はVue CLIを使用し、ライブラリのバージョンは5.0.0-beta.2としています。
Nodeのバージョンは15.8.0としました。

❯ node -v
v15.8.0

❯ vue create my-project

// 以下設定値(マニュアルで設定しましたが、Vue2であればおそらく今回は問題ないかと思います)
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Linter, Unit, E2E
? Choose a version of Vue.js that you want to start the project with 2.x
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Pick an E2E testing solution: Cypress
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

サンプルプロジェクト

それっぽい構成を用意します。

コンポーネント

App.vue
<template>
  <div>
    <table v-if="!loading">
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>Company</th>
          <th>isAdmin</th>
        </tr>
      </thead>
      <tbody>
        <UserRow v-for="user in users" :user="user" :key="user.id" />
      </tbody>
    </table>
    <div v-if="loading">ロード中</div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { User } from "./@types";
import UserRow from "./components/UserRow.vue";
import { sleep } from "./utils";

@Component({
  components: {
    UserRow,
  },
})
export default class App extends Vue {
  users: User[] = [];
  loading = true;

  async mounted(): Promise<void> {
    // 3秒掛けてAPIでデータを取ってくるようなイメージ
    this.users = await new Promise<User[]>((resolve) => {
      sleep(3).then(() => {
        resolve([
          { id: 1, name: "Bob", company: "Company A", isAdmin: true },
          { id: 2, name: "Kate", company: "Company B", isAdmin: false },
        ]);
      });
    });
    this.loading = false;
  }
}
</script>
UserRow.vue
<template>
  <tr>
    <td>{{ user.id }}</td>
    <td>{{ user.name }}</td>
    <td>{{ user.company }}</td>
    <td>{{ user.isAdmin }}</td>
  </tr>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { User } from "../@types";

@Component
export default class UserRow extends Vue {
  @Prop() private user!: User;
}
</script>

こんな感じの画面です。

@types/index.d.ts
export type User = {
  id: number;
  name: string;
  company: string;
  isAdmin: boolean;
};

その他(ローディングをわかりやすくするためのsleep処理)

utils/index.ts
export function sleep(sec: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, sec * 1000));
}

CompositionAPIを導入する

ライブラリインストール

yarn add @vue/composition-api

プラグインとして読み込む

main.tsで読み込みます。

main.ts
import Vue from "vue";
+ import VueCompositionApi from "@vue/composition-api";
import App from "./App.vue";

+ Vue.use(VueCompositionApi);

Vue.config.productionTip = false;

new Vue({
  render: (h) => h(App),
}).$mount("#app");

これで準備オッケーです。

リファクタリングしてみる

App.vue内のロジックを外に切り出してみます。
新しくファイルを作成します。

composable/useUserData.ts
import { User } from "@/@types";
import { sleep } from "@/utils";
import { Ref, ref } from "@vue/composition-api";

export function useUserData(): { loading: Ref<boolean>; users: Ref<User[]> } {
 const loading = ref<boolean>(true);
 const users = ref<User[]>([]);

 const fetchData = async () => {
   const res = await new Promise<User[]>((resolve) => {
     sleep(3).then(() => {
       resolve([
         { id: 1, name: "Bob", company: "Company A", isAdmin: true },
         { id: 2, name: "Kate", company: "Company B", isAdmin: false },
       ]);
     });
   });
   users.value = res;
   loading.value = false;
 };
 fetchData();

 return { loading, users };
}

次にこれを利用するようApp.vueを書き換えます。
class-styleで書かれていますが、defineComponentを使うようにしてみます。

App.vue
<template>
 <div>
   <table v-if="!loading">
     <thead>
       <tr>
         <th>ID</th>
         <th>Name</th>
         <th>Company</th>
         <th>isAdmin</th>
       </tr>
     </thead>
     <tbody>
       <UserRow v-for="user in users" :user="user" :key="user.id" />
     </tbody>
   </table>
   <div v-if="loading">ロード中</div>
 </div>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";
import { useUserData } from "@/composable/useUserData";
import UserRow from "./components/UserRow.vue";

export default defineComponent({
 components: {
   UserRow,
 },
 setup() {
   const { loading, users } = useUserData();
   return {
     users,
     loading,
   };
 },
});
</script>

何が嬉しいのか

今回はシンプルな構成なので、伝わりにくい部分もあるかと思いますが、
ある程度の規模になってくると、コンポーネントを上手く分けたとしても
結局子コンポーネントから親コンポーネントの情報を書き換えたりすることがあったりして
親コンポーネント(特にAtomic DesignにおけるTemplatesPagesなど)がもつ情報量が多くなりがちです。

そういったときに関連するデータやメソッドをまとめて扱えると便利です。
今回の場合はloadingusersuseUserDataという関数の中でまとめてあつかい、App.vueからなるべく切り出しています。

上手く画面から情報を切り出して見通しの良い実装になるように心がけたいですね。

参考

https://v3.vuejs.org/api/composition-api.html#composition-api
https://github.com/vuejs/composition-api

GitHubで編集を提案

Discussion