LoginSignup
15
13

More than 3 years have passed since last update.

【Vue.js】CompositionAPIでTODOリスト

Last updated at Posted at 2020-07-25

もうすぐVue3が出ると聞いたので、目玉機能のひとつ「CompositionAPI」を試してみました。
題材は定番のTODOリスト(の超簡易版)です。


CompositionAPIとは

a set of additive, function-based APIs that allow flexible composition of component logic.
(公式より引用)

  • Vue3で正式リリース予定の便利機能
  • data, method, computed, ライフサイクルメソッドなどを関数として定義できる
    • よってTypeScriptで型付けしやすい
    • thisを使う必要がなくなる
    • 機能Aのdata, method, computed => 機能Bのdata, computed ...のように、関心の単位で処理をまとめられる
    • 関数をexport/importして、コンポーネント間でロジックを再利用できる
    • 柔軟な構成(Composition)でコードを配置できることが名前の由来
    • 要するに、VueにおけるReact Hooks的なもの
      (公式にもReact Hooksから着想を得て作られた と書いてある)
  • 2系でも使える(npm install @vue/composition-apiすればOK)
    • 従来のOptionsAPI(data, method, computed)と併用可能 (多少の制限はある)
    • Vue3系になっても、OptionsAPIとCompositionAPIは共存していく方針とのこと


メリット

  • ロジックを再利用しやすい
  • 柔軟な構成でコードを配置できる
  • TypeScriptによる型付けをしやすい 1.png
    (柔軟な構成で書けるから役割ごとにコードをまとめられるよ! という公式の図)

デメリット

  • 柔軟な書き方ができる分、好き放題できてしまえる
    (コード品質の上限を引き上げるが、下限も引き下げる。公式では、上限を引き上げるメリットのほうが大きいと言っている)
  • 現状だとベストプラクティス的な書き方がなく、コード品質が書き手にかなり依存する





...というわけで、以下は試してみたコードです。

Vue2系でCompositionAPIを使えるようにする

参考: https://github.com/vuejs/composition-api

インストール

$ npm install -D @vue/composition-api

Vue.use()する

main.ts
 import VueCompositionApi from '@vue/composition-api';
 Vue.use(VueCompositionApi);


超簡易版のTODOリストを作成

スクリーンショット 2020-07-25 21.03.50.png
スクリーンショット 2020-07-25 21.04.06.png

Todos.vue
<template>
  <div class="container">
    <h2>TODO LIST</h2>

    <form class="todo-form">
      <span>何をやる?</span>
      <input type="text" v-model="state.nameForSubmit" />
      <span>いつまでにやる?</span>
      <input type="date" v-model="state.duedateForSubmit" />
      <button @click="addTodo">リストに追加</button>
    </form>

    <div class="todo-list">
      <input type="checkbox" @change="toggleShouldExtractIncompleteTodos" />
      未完了のTODOだけ表示
      <div v-for="todo in todosToShow" :key="todo.id">
        <div class="todo-list-item">
          {{ 'id(' + todo.id + '), name(' + todo.name + '), duedate(' + todo.duedate + '), isDone(' + (todo.isDone === true ? '完了' : '未完了') + ')' }}
        </div>
        <button @click="toggleIsDone(todo.id)">
          {{ todo.isDone === true ? '未完了にする' : '完了させる' }}
        </button>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { reactive, computed, defineComponent } from '@vue/composition-api';

interface TODO {
  id: number;
  name: string;
  duedate: number;
  isDone: boolean;
}

// defineComponent()を使うことで、コンポーネント化できる (使わずに、共通ロジックを定義することもできる)
export default defineComponent({
  // setup()は読み込み時に1度だけ実行される  ←Hooksとの相違点
  setup() {
    // 従来のdataに相当する部分
    //  * ひとつの変数に全部突っ込むこともできるが、せっかくなのでtemplateへ共有する状態、しない状態を分けてみた
    //  * reactive()を使うことでリアクティブになる
    //  * 似たような用途の関数にref()があるが、今回の範囲では使わなかった
    const privateState = reactive({
      todos: [] as TODO[],
      idForSubmit: 1,
    });
    const publicState = reactive({
      nameForSubmit: '',
      duedateForSubmit: 0,
      shouldExtractIncompleteTodos: false,
    });

    // 従来のmethodに相当する部分
    //  * もちろんstateには自由にアクセスできる
    //  * "this"を使っていない点に注目
    const addTodo = (): void => {
      privateState.todos.push({
        id: privateState.idForSubmit++,
        name: publicState.nameForSubmit,
        duedate: publicState.duedateForSubmit,
        isDone: false,
      });
    };
    const toggleIsDone = (id: number): void => {
      privateState.todos.forEach((todo) => {
        if (todo.id === id) {
          todo.isDone = !todo.isDone;
        }
      });
    };
    const toggleShouldExtractIncompleteTodos = (): void => {
      publicState.shouldExtractIncompleteTodos = !publicState.shouldExtractIncompleteTodos;
    };

    // 従来のcomputedに相当する部分
    //  * reactive()を使わないとリアクティブにならないので注意
    //  * 一応、const todosToShow = computed(...); と書くこともできる。リアクティブにしたくないときに使えるのかも?
    const todosToShow = reactive(
      computed((): TODO[] => {
        if (publicState.shouldExtractIncompleteTodos === true) {
          return privateState.todos.filter((todo) => {
            return todo.isDone === false;
          });
        }
        return privateState.todos;
      })
    );

    // setup()の最後に色々返す。ここで返したものはtemplateから参照できる。
    return {
      state: publicState,
      addTodo,
      toggleIsDone,
      toggleShouldExtractIncompleteTodos,
      todosToShow,
    };
  },
});
</script>

<style lang="scss" scoped>
.container {
  border: solid 1px;
  margin: 50px;
  padding: 8px;
}

.todo-form {
  input {
    border: solid 1px;
    border-radius: 4px;
    display: block;
    margin-bottom: 8px;
    width: 200px;
  }
  button {
    background-color: aliceblue;
    border: solid 1px;
    border-radius: 4px;
    margin-right: 8px;
    margin-top: 8px;
    width: 200px;
  }
}

.todo-list {
  margin-top: 16px;
  .todo-list-item {
    display: inline-block;
    width: 500px;
  }
  button {
    background-color: aliceblue;
    border: solid 1px;
    border-radius: 4px;
    width: 200px;
  }
}
</style>


おまけ: コンポーネント間でpropsを渡す

1.png

Parent.vue
<template>
  <div class="parent-component">
    <h2>Parent Component</h2>
    count
    <div>{{ state.count }}</div>
    count * 2
    <div>{{ state.double }}</div>

    <br>
    <button @click="increment" class="button">
      increment(parent)
    </button>

    <br>
    <ChildComponent
      :count="state.count"
      :increment="increment"
    />
  </div>
</template>

<script lang="ts">
import { reactive, computed, onMounted, defineComponent } from "@vue/composition-api";
import ChildComponent from "@/containers/ChildComponent.vue"

export default defineComponent({
  components: {
    ChildComponent
  },
  setup() {
    // setup(): top => setup(): bottom => onMounted() の順で呼び出される
    console.log('setup(): top');
    onMounted(() => {
      console.log('onMounted()')
    })

    const state = reactive({
      count: 1,
      double: computed((): number => {
        return state.count * 2;
      }),
    })
    const increment = (): void => {
      state.count++;
    }

    console.log('setup(): bottom');
    return {
      state,
      increment,
    };
  }
});
</script>

<style lang="scss" scoped>
.parent-component {
  margin-left: 100px;
  border: solid 1px;
  padding: 5px;
}
.button {
  border: solid 1px;
  background-color: aliceblue;
  border-radius: 4px;
  width: 200px;
}
</style>
Child.vue
<template>
  <div class="child-component">
    <h2>Child Component</h2>
    count * count
    <div>{{ state.power }}</div>

    <br>
    <button @click="increment" class="button">
      increment(child)
    </button>
  </div>
</template>

<script lang="ts">
import { reactive, computed, defineComponent } from "@vue/composition-api";

type Props = {
  count: number;
  increment: () => void;
};

export default defineComponent({
  props: {
    count: {
      type: Number,
      required: true,
    },
    increment: {
      type: Function,
      required: true,
    }
  },
  setup(props: Props) {
    const state = reactive({
      power: computed((): number => {
        return props.count * props.count;
      }),
    })
    return {
      state,
    };
  }
});
</script>

<style lang="scss" scoped>
.child-component {
  border: solid 1px;
  padding: 5px;
  margin-top: 20px;
}
.button {
  border: solid 1px;
  background-color: aliceblue;
  border-radius: 4px;
  width: 200px;
}
</style>


やってみた感想

  • 形式は違えど、Vueだけあってかなり直感的に書ける
  • 普段OptionsAPIでやってるのと同様のことができそうという感覚が得られた
  • 関数で書けるのはやっぱり便利
  • 確かに柔軟に書ける
  • けど無法地帯になりうるのも確かで、ベストプラクティスが出回るまでは無理に導入しなくてもいいかなという印象
  • reactive()とref()の使い分けがまだピンと来ていない
    • 公式曰く「CompositionAPIを効果的に使うには、両者を使い分けなければならない」らしい
    • とりあえず超簡易TODOリスト程度の範囲では、reactive()だけで困ることがなかった
    • この辺は追々学んでいこう



以上です。
Vue3が順調にリリースされますように。

15
13
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
15
13