Nuxt Composition APIでv2との書き方の違いを確認した

Composition APIを使うことになったので、今までの書き方がComposition APIではどう書けばいいのか確認した。
1つのコンポーネント内での違い(data, computed, methods, watch, created, mounted)と、親子のコンポーネント間の違い(v-model, prop, emit, sync)をコードを書いて確認している。

プロジェクトでNuxtを使うことが多いので、nuxt-appでアプリケーションを初期化し、@nuxtjs/composition-apiを入れて動かした。

下準備

npm init nuxt-appでNuxtアプリケーションを作成した。
cssフレームワークやlint、formatterはお好みでいれる。
別途@nuxtjs/composition-apiをインストールし、nuxt.config.jsbuildModulesに設定する。

$ npm init nuxt-app composition-api-example
$ cd composition-api-example
$ gibo dump Node VisualStudioCode >> .gitignore
$ npm run dev
$ npm install @nuxtjs/composition-api --save

nuxt.config.js

{
  buildModules: [
    '@nuxtjs/composition-api',
    // 他...
  ]
}

1つのコンポーネント内での違い data, computed, methods, watch, created, mounted

今まではdataプロパティやmethodscreatedのようなライフサイクルフックなどそれぞれ分けて定義していたが、Composition APIではすべてsetup関数の中で定義する。

data → ref, reactive

dataComposition APIrefあるいはreactiveで表現される。
refはプリミティブな値を管理し、reactiveはオブジェクトや配列を管理する。
そのため、reactiveの方が今までの使い方に近い。
ただし、refにオブジェクトや配列を渡すと、内部でreactiveが呼ばれるため問題なく使える。

次のコードはdatareactiveで管理し、titlerefで管理している。
setTimeoutdatatitleの値が変わったら、テンプレートの値も変わることを確認している。

<template>
  <div>
    <h1>{{ title }}</h1>
    <ul>
      <li v-for="u in data.users" :key="u.id">{{ u.id }} {{ u.name }}</li>
    </ul>
  </div>
</template>

<script lang="js">
import { defineComponent, reactive, ref } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const data = reactive({
      users: [
        { id: 1, name: '加藤かな' },
        { id: 2, name: '田中紘一' },
        { id: 3, name: '山田太郎' },
      ],
    })
    const title = ref("タイトル")

    setTimeout(() => {
      data.users.push({ id: 4, name: '新藤誠' })
      title.value = "タイトルが変更できること"
    }, 1000);

    return {
      data,
      title,
    }
  },
})
</script>

computed → computed

computed(算出プロパティ)はsetup関数のなかで呼び出す。
今まではthis.usersのようにthisを経由してもとになる値を参照していたが、 Composition APIではsetup関数内に定義された変数をそのまま参照する。

次のコードはdata.usersの件数をもとにuserNumというユーザー件数を算出して表示できるようにしている。

<template>
  <div>
    <p>ユーザー件数: {{ userNum }}</p>
  </div>
</template>

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

export default defineComponent({
  setup() {
    const data = reactive({
      users: [
        { id: 1, name: '加藤かな' },
        { id: 2, name: '田中紘一' },
        { id: 3, name: '山田太郎' },
      ],
    })
    const userNum = computed(() => data.users.length)
    return {
      data,
      userNum,
    }
  },
})
</script>

methods → 普通の関数

methodscomputedのようなVue独自の決まりにしたがって書く必要はなく普通の関数として書く。 次のコードは「ユーザー追加」ボタンを押すとdata.usersにユーザを追加するaddUser関数を定義している。

<template>
  <div>
    <form @submit.prevent>
      <div>
        <label for="name">お名前</label>
        <input id="name" type="text" v-model="data.form.name">
      </div>    
      <div>
        <button @click="addUser">ユーザー追加</button>
      </div>
    </form>
    <div style="margin-top:16px;">
      <p>ユーザー件数: {{ userNum }}</p>
      <ul>
        <li v-for="u in data.users" :key="u.id">{{ u.id }} {{ u.name }}</li>
      </ul>
    </div>
  </div>
</template>

<script lang="js">
import { defineComponent, reactive, computed, ref } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const data = reactive({
      form: {
        name: '',
      },
      users: [
        { id: 1, name: '加藤かな' },
        { id: 2, name: '田中紘一' },
        { id: 3, name: '山田太郎' },
      ],
    })
    const userNum = computed(() => data.users.length)
    const addUser = () => {
      const id = Math.max(...data.users.map(u=>u.id)) + 1
      data.users.push({id: id, name: data.form.name})
      data.form.name = ''
    }

    return {
      data,
      addUser,
      userNum,
    }
  },
})
</script>

なお、今までのdataは1つしかなかったが、Composition APIではreactiveの部分は1つにしなければいけないというルールはないため、2つ以上定義してもよい。

    const data = reactive({
      users: [
        { id: 1, name: '加藤かな' },
        { id: 2, name: '田中紘一' },
        { id: 3, name: '山田太郎' },
      ],
    })
    const form = reactive({
      name: '',
    })

watch → watch, watchEffect

watchComposition APIwatchあるいはwatchEffectで定義する。
watchの方が今までと同じようにプロパティを監視して何らかの処理を実行できる。
一方でwatchEffectはプロパティを指定せず、第1引数に渡すコールバック関数で使われる値を監視して何らかの処理を実行する。
ここでは明示的にプロパティを指定するwatchを使う。

次のコードはwatch内の第1引数で監視するプロパティを指定し、第2引数でプロパティの値が変更された際の値をconsoleに表示している。
第2引数のコールバックは第1引数が変更後の値、第2引数が変更前の値になる。

<template>
  <div>
    <form @submit.prevent>
      <div>
        <label for="name">お名前</label>
        <input id="name" v-model="form.name" type="text" />
      </div>
      <div>
        <label for="email">email</label>
        <input id="email" v-model="form.email" type="text" />
      </div>
    </form>
  </div>
</template>

<script lang="js">
import { defineComponent, reactive, computed, ref, watch } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const form = reactive({
      name: '',
      email: '',
    })

    watch(
      () => form.name,
      (currentName, prevName) => {
        console.info('currentName: ', currentName, 'prevName: ', prevName)
      },
    )

    return {
      form,
    }
  },
})
</script>

オブジェクトの各プロパティを監視したい場合は第3引数に{deep: true}を追加する。

    watch(
      () => form,
      (currentForm, prevForm) => {
        console.info('currentForm: ', currentForm, 'prevForm: ', prevForm)
      },
      { deep: true},
    )

created → 普通の関数呼び出しとして書く

createdcomputedのようなVue独自の決まりにしたがって書く必要はなく普通の関数として書く。
createdはDOMを参照できないが、setup関数内の変数は参照することができるため、APIによる値の初期化などに使われる。

次のコードはAPIでdataを初期化するのを模倣して、setTimeoutで3秒後に初期化されることを確認している。

<template>
  <div>
    <div style="margin-top: 16px">
      <p>ユーザー件数: {{ userNum }}</p>
      <ul>
        <li v-for="u in data.users" :key="u.id">{{ u.id }} {{ u.name }}</li>
      </ul>
    </div>
  </div>
</template>

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

export default defineComponent({
  setup() {
    const data = reactive({
      users: [],
    })

    const userNum = computed(() => data.users.length)

    // created ... DOMにさわれることが保証されてない。APIからデータ取得する処理などを書く
    setTimeout(() => {
      data.users.push(...[
        { id: 1, name: '加藤かな' },
        { id: 2, name: '田中紘一' },
        { id: 3, name: '山田太郎' },
      ])
    }, 3000);

    return {
      data,
      userNum,
    }
  },
})
</script>

mounted → onMounted

mountedComposition APIではonがつき、onMountedになった。
onMountedはDOMを参照できる。
DOMの参照は、今まではthis.$refs.emailInputのようにthis.$refsで参照していた。
しかし、Composition APIではref(null)setup関数内に変数を用意しておいて、 テンプレートでrefによりその変数と紐づけをすることでDOMを参照できる。

次のコードはメールアドレスのinputタグにフォーカスが当たるようにしている。

<template>
  <div>
    <form @submit.prevent>
      <div>
        <label for="email">email</label>
        <input ref="emailInput" id="email" type="text" />
      </div>
    </form>
  </div>
</template>

<script lang="js">
import { defineComponent, reactive, onMounted, ref } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const emailInput = ref(null);

    onMounted(()=>{
      // composition api以前は this.$refs.emailInputのような形で取得していた
      emailInput.value.focus()
    })
    console.info("before mount", emailInput.value)

    return {
      emailInput,
    }
  },
})
</script>

親子のコンポーネント間の違い - v-model, prop, emit, sync

props → props

親コンポーネントから子コンポーネントへの渡し方は:users="data.users"のように渡す。親コンポーネントはComposition APIによる違いはない。

子コンポーネントでpropsの値を使って何らかの処理をするにはsetupの第1引数からpropsを取得して操作するようになっている。

次のコードは親コンポーネントindex.vueから子コンポーネントUserList.vueへユーザーの一覧を渡して表示している。
子コンポーネントUserList.vueではsetup関数内でpropsでわたってきたユーザーの一覧からユーザー数を算出している。

親コンポーネントindex.vue

<template>
  <div>
    <UserList :users="data.users" />
  </div>
</template>

<script lang="js">
import { defineComponent, reactive } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const data = reactive({
      users: [
        { id: 1, name: '加藤かな' },
        { id: 2, name: '田中紘一' },
        { id: 3, name: '山田太郎' },
      ],
    })

    setTimeout(() => {
      data.users.push({ id: 4, name: '木下武' })
    }, 3000);

    return {
      data
    }
  },
})
</script>

子コンポーネントUserList.vue

<template>
  <div>
    <p>ユーザー件数: {{ userNum }}</p>
    <ul>
      <li v-for="u in users" :key="u.id">{{ u.id }} {{ u.name }}</li>
    </ul>
  </div>  
</template>

<script lang="js">
import { defineComponent, computed } from '@nuxtjs/composition-api'

export default defineComponent({
  props: {
    users: {
      type: Array,
      required: true
    }
  },
  setup(props) {
    const userNum = computed(() => props.users.length)
    return {
      userNum
    }
  },
})
</script>

v-model → v-model, this.$emit → context.emit

親コンポーネントから子コンポーネントへ値を渡し、子コンポーネントから親コンポーネントへ変化した値を返すには、親コンポーネントでv-model="値"のようにする。親コンポーネントはComposition APIによる違いはない。
なお、Vueのv3ではv-model破壊的変更 (opens new window)があるようだがこちらはふれない。

子コンポーネントではinputタグのインラインで$emitする方法と、setup関数でcontext.emitする方法がある。前者はComposition APIによる違いはない。 後者はsetup関数の第2引数からcontextを取得しcontext.emitする。第2引数をsetup(_, {emit})のように分割代入して、setup関数内でemit単体で扱ってもよい。

次のコードは親コンポーネントindex.vueから子コンポーネントInputName.vueへ値を渡し、InputName.vue内のinput type="text"inputイベントが起こるたびに親コンポーネントへ変化した値を渡している。
v-modelはinputのtypeごとに挙動が違い、ここではinputのtypeがtextの場合を示している。

親コンポーネントindex.vue

<template>
  <div>
    <form @submit.prevent>
      <InputName v-model="form.name" />
    </form>
  </div>
</template>

<script lang="js">
import { defineComponent, reactive } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const form = reactive({
      name: '山田',
    })

    return {
      form
    }
  },
})
</script>

子コンポーネントInputName.vue inputタグでインラインに$emitする書き方

<template>
  <div>
    <label for="name">お名前</label>
    <input id="name" :value="value" @input="$emit('input', $event.target.value)" type="text" />
  </div>
</template>

<script lang="js">
import { defineComponent, toRefs, isRef } from '@nuxtjs/composition-api'

export default defineComponent({
  props: {
    value: {
      type: String,
      required: true
    },
  },
})
</script>

子コンポーネントInputName.vue setup関数でcontext.emitする書き方

<template>
  <div>
    <label for="name">お名前</label>
    <input id="name" :value="value" type="text" @input="updateValue" />
  </div>
</template>

<script lang="js">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  props: {
    value: {
      type: String,
      required: true
    },
  },
  setup(_, context) {
    const updateValue = (e) => {
      context.emit('input', e.target.value)
    }
    return {
      updateValue
    }
  },
})
</script>

子コンポーネントInputName.vue setup関数で第2引数を分割代入してemit単体で扱う書き方(一部抜粋)

  setup(_, {emit}) {
    const updateValue = (e) => {
      emit('input', e.target.value)
    }
    return {
      updateValue
    }
  },

.sync → .sync

親コンポーネントから子コンポーネントへ値を渡し、子コンポーネントから親コンポーネントへ変化した値を返すには、親コンポーネントでv-model="値"とする他に:prop名.sync="値"とも書ける。

子コンポーネントでは@input="$emit('update:prop名', $event.target.value)"あるいはsetup関数でemit('update::prop名', e.target.value)のように書く。

親コンポーネントindex.vue

<template>
  <div>
    <form @submit.prevent>
      <InputName :value.sync="form.name" />
    </form>
  </div>
</template>

<script lang="js">
import { defineComponent, reactive } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const form = reactive({
      name: '山田',
    })

    return {
      form
    }
  },
})
</script>

子コンポーネントInputName.vue inputタグでインラインに$emitする書き方

<template>
  <div>
    <label for="name">お名前</label>
    <input
      id="name"
      :value="value"
      type="text"
      @input="$emit('update:value', $event.target.value)"
    />
  </div>
</template>

<script lang="js">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  props: {
    value: {
      type: String,
      required: true
    },
  },
})
</script>

子コンポーネントInputName.vue setup関数でemitする書き方

<template>
  <div>
    <label for="name">お名前</label>
    <input id="name" :value="value" type="text" @input="updateValue" />
  </div>
</template>

<script lang="js">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  props: {
    value: {
      type: String,
      required: true
    },
  },
  setup(_, {emit}) {
    const updateValue = (e) => {
      emit('update:value', e.target.value)
    }
    return {
      updateValue
    }
  },
})
</script>

参考
https://v3.vuejs.org/guide/composition-api-introduction.html#why-composition-api (opens new window)