Vue 3 Composition APIの基本的な使い方

Vue.js

Vue 3から新しく導入されたComposition APIの基本的な使い方をまとめました。

Vue 3 Composition APIの基本的な使い方

Options APIとComposition APIの違い

基本的な使い方を知る前に、Composition APIを使う理由を知っておきましょう。 従来のOptions APIと比較してComposition APIを使うことで、散らばっていきたロジックをグループ化しやすくなります。 再利用しやすいコンポーネントを作成できる といったメリットがあります。 大規模になればなるほどメリットがみえやすくなります。 基本的な違いは、setup()を使う点です。
<script>
export default {
  setup() {

    // data

    // methods

    // computed

    // lifecycle hooks
  }
}
</script>
setup()の中にdata、methods、computed、ライフサイクルフックをまとめて記述することができます。

変数の宣言・関数

<template>
  <div class="home">
    Home
    <p>私の名前は{{ name }}です。{{ age }}才です。</p>
    <!-- 私の名前は、山田です。50才です。 -->
    <button @click="handleClick">クリック</button>
  </div>
</template>
template内は従来と同じです。
<script>
export default {
  name: 'Home',
  setup() {
    // setupで次のように宣言された変数はリアクティブではない
    let name = '山田' 
    let age = 50

    const handleClick = () => {
      console.log('Clicked');
    }

    // return { name: name, age: age} //左がtemplate、右がsetupで宣言した変数
    return { name, age, handleClick} //上のように同じ場合はこのように省略できる
  },
  data() {
    return {
      // age: 45 //リアクティブ
    }
  }
}
</script>
上のように、変数や関数を宣言して使うことができます。 変数や関数はreturnする必要があります。 左がtemplate、右がsetupで宣言した変数を意味します。 同じ場合は省略可能です。 また、注意点としては、上のように宣言した変数は、data()でreturnされるときのようなリアクティブではありません。 リアクティブにする方法は、ref関数の下で説明します。

Composition APIでref関数を使う

DOM要素を参照して、スタイルやプロパティを取得・操作したい場合に使う$refsですが、Composition APIでは使い方が異なります。 templeteタグでは、
    <p ref="p">私の名前は{{ name }}です。{{ age }}才です。</p>
refはpタグなのでpとしておきます。
<script>
import { ref } from 'vue'

export default {
  name: 'Home',
  setup() {
    console.log(this); //undefined

    const p = ref(null)
    
    // setupで次のように宣言された変数はリアクティブではない
    let name = '山田' 
    let age = 50

    const handleClick = () => {
      console.log(p); // Refのオブジェクトが返される。その中のvalueにDOMが格納されている
      console.log(p.value); // <p>私の名前は山田です。50才です。</p>
      p.value.classList.add('test')
      p.value.textContent = 'test'
  //<p class="​"test"">​test​</p>

​ } return { name, age, handleClick, p} } } </script>
ref(null)としてref関数を変数に格納します。今回はpタグなので変数pに格納しています。 returnでもpが返されるように記述します。 そして、vueからrefをimportします。 そうすることで、あとは通常のJavaScriptのDOM操作と同様のことができるようになります。 returnする前のプロパティのvalueはnullになるので、必ずreturnする必要がある点を忘れないようにしましょう。

setup()の変数をリアクティブにする

setup()の中で宣言された変数はリアクティブではない

data()の中でセットされたプロパティはデフォルトでリアクティブになりますが、 setup()の中で宣言された変数はリアクティブではありません。
<template>
  <div class="home">
    Home
    <p ref="p">私の名前は{{ name }}です。{{ age }}才です。</p>
    <!-- 私の名前は、山田です。50才です。 -->
    <button @click="handleClick">クリック</button>
  </div>
</template>
<script>
export default {
  name: 'Home',
  setup() {
    // setupで次のように宣言された変数はリアクティブではない
    let name = '山田' 
    let age = 50

    const handleClick = () => {
      name = '佐々木' //クリックしても山田が佐々木に変わらない
    }

    return { name, age, handleClick, p}
  }
}
</script>
上の場合、クリックしても表示される名前は変更されません。
<script>
  data() {
    return {
      name: '佐々木' //これはリアクティブ
    }
  }
</script>
こちらのdata()を使うとデフォルトでリアクティブなため、変更されます。 setup()の中の変数をリアクティブにするには2つの方法があります。

方法1: ref関数でリアクティブにする

<template>
  <div class="home">
    Home
    <p>私の名前は{{ name }}です。{{ age }}才です。</p>
    <!-- 私の名前は、山田です。50才です。 -->
    <button @click="handleClick">クリック</button>
    <!-- 私の名前は佐々木です。45才です。 -->
  </div>
</template>
templateでは変数をそのまま入れるだけでOKです。 name.valueなどとする必要はありません。
<script>

import { ref } from 'vue'

export default {
  name: 'Home',
  setup() {
    // setupでリアクティブにするにはref()を使う
    const name = ref('山田')
    const age = ref(50)

    const handleClick = () => {
      name.value = '佐々木'
      age.value = 45
    }

    return { name, age, handleClick }
  }
}
</script>
このようにして、変数にはref()関数を使った形で格納します。 ref関数のため、アクセスするにはname.valueのようにvalueを使います。 あとは通常のJavaScriptの操作で使用することができます。 これでボタンをクリックすると名前と年齢がアップデートされます。
<template>
  <div class="home">
    Home
    <p>私の名前は{{ name }}です。{{ age }}才です。</p>
    <button @click="handleClick">クリック</button>
    <button @click="age++">年齢を上げる</button>
    <button @click="age--">年齢を下げる</button>
    <input type="text" v-model="name">
  </div>
</template>
templateにinputタグなどでv-modelで紐付けるときも、name.valueではなくnameとします。

方法2: reactiveでリアクティブにする

まず、reactiveはvalueにアクセスしないため、プリミティブ型の値はリアクティブに変更することができません。 また、外部関数を作成するときもrefのほうが優れているので、現状ではreactiveは使うことは少ないでしょう。 refと似たような使い方となるので、下のように併記します。
<template>
  <div class="home">
    Home
    <p>私の名前は{{ name }}です。{{ age }}才です。</p>
    <button @click="handleClick">クリック</button>
    <p>私の名前は{{ student.name2 }}です。{{ student.age2 }}才です。</p>
    <button @click="handleClickTwo">クリック2</button>
  </div>
</template>
まず、reactiveを使う場合、プリミティブ型の値ではリアクティブにならないのでオブジェクトに格納します。
<script>

import { ref, reactive } from 'vue'

export default {
  name: 'Home',
  setup() {
    // ref
    const name = ref('山田')
    const age = ref(50)

    // reactive
    const student = reactive({name2: '近藤', age2: 20})
    // プリミティブ値の場合リアクティブにならない
    // const name2 = reactive('近藤')
    // let age2 = reactive(20)

    // ref
    const handleClick = () => {
      name.value = '佐々木'
      age.value = 45
    }
    // reactive
    const handleClickTwo = () => {
      student.name2 = '斎藤'
      student.age2 = 25
    }

    return { name, age, handleClick, student, handleClickTwo }
  }
}
</script>
refと同じように、vueからimportします。 reactiveには、オブジェクトを格納します。 一番の違いが関数です。 ref関数のようにname2.valueとアクセスするのではなく、変数をそのまま指定します。 よってプリミティブ型の場合は参照先を変更するわけではないので、値を変更することができません。 上記のようにオブジェクトにいれることで、値の参照先を変更することができます。

Computed()を使う

シンプルな基本形

<template>
  <div class="home">
    Home
    <p>私の名前は{{ name }}です。</p>
  </div>
</template>
<script>
import { ref, computed } from 'vue'

export default {
  name: 'Home',
  setup() {
    const name = computed(() => {
      return '山田'
    })
    return { name }
  }
}
</script>
シンプルな基本の形は上のようになります。 computedをvueからimportします。 setup()の中で、変数にcomputed()を格納します。 computed()でもsetup()でもreturnします。 それでは、具体的な使い方をみてみましょう。

具体的な使い方

<template>
  <div class="home">
    <h1>Home</h1>
    <input type="text" v-model="search" />
    <p>検索 - {{ search }}</p>
    <div v-for="name in matchingNames" :key="name">{{ name }}</div>
  </div>
</template>
検索すると名前の配列から該当の名前のみが表示されるようにします。 まずはinputタグをv-modelを使ってインタラクティブにします。 v-forでは、computedを格納した変数をループします。 searchのキーワードでフィルターするのは、computedでfilter()メソッドを使います。
<script>

import { ref, computed } from 'vue'

export default {
  name: 'Home',
  setup() {
    const search = ref('')
    const names = ref([
      '山田',
      '佐々木',
      '山川',
      '山岡',
      '川田',
      '近藤',
      '斎藤'
    ])

    const matchingNames = computed(() => {
      // namesはrefオブジェクトなので、valueを使う
      return names.value.filter(name => name.includes(search.value))
    })
    return { names, search, matchingNames }
  }
}
</script>
まずは、computedをimportします。 そして、setup()の中でcomputed()を格納する変数を作ります。 filter()でアクセスするときは、配列はrefオブジェクトになっているので、valueを使ってアクセスします。 そして、忘れずにその変数をreturnします。

watch()とwatchEffect()

watch()とwatchEffect()も、監視対象に変更があるたびに関数が反応して、変更の検知をすることができます。
<template>
  <div class="home">
    <h1>Home</h1>
    <input type="text" v-model="search" />
    <p>検索 - {{ search }}</p>
    <div v-for="name in matchingNames" :key="name">{{ name }}</div>
    <button @click="handleClick">stop</button>
  </div>
</template>
watchを停止するために、handleClickというイベントを作っておきます。
<script>

import { ref, computed, watch, watchEffect } from 'vue'

export default {
  name: 'Home',
  setup() {
    const search = ref('')
    const names = ref([
      '山田',
      '佐々木',
      '山川',
      '山岡',
      '川田',
      '近藤',
      '斎藤'
    ])

    // 監視する対象を明示的に記述する
    const stopWatch = watch(search, () => {
      console.log('watch function route')
    })

    // watchEffectは明示的に記述する必要がないが、関数内で監視対象を使用する必要がある
   const stopEffect = watchEffect(() => {
      console.log('watchEffect function run', search.value)
    })

  const handleClick = () => {
    stopWatch()
    stopEffect()
  }

    return { names, search, matchingNames, handleClick }
  }
}

</script>
watch, watchEffectをimportします。 watch(), watchEffect()をストップする際に変数として格納されていると便利なので、変数を用意します。 watch()の場合は、監視する対象を第一引数に、明示的に記述します。そして、第二引数で関数をとります。 watchEffect()は明示的に記述する必要はありませんが、関数内で監視対象を使用する必要があります。 watchEffect()の中でwatchしてほしい値をいれるとwatch()と同じように変化があるたびに反応します。 もし、watchEffect()の関数の中に監視対象を含めていない場合、初期に一回だけ反応します。 監視する対象を関数内で使用しない場合はwatch、使用する場合はwatchEffectという使い分けでもいいでしょう。

propsを使う

views/Home.vueでcomponents/PostList.vueを使うとします。 そして、components/PostList.vueでcomponents/SinglePost.vueを使うとします。 まずは、componentsフォルダにPostList.vueとSinglePost.vueを作成します。

ディレクトリ構造

src
├─ App.vue
├─components
│ ├─ PostList.vue
│ └─ SinglePost.vue
├─views
│ └─ Home.vue

viewsのvueファイルとコンポーネントをつなげる

まずは、Home.vueとPostList.vueをつなげます。

views/Home.vue

<template>
  <div class="home">
    <h1>Home</h1>
    <PostList :posts="posts" />
  </div>
</template>
PostListのコンポーネントを記述します。
<script>

import PostList from '../components/PostList.vue'
import { ref } from 'vue'

export default {
  name: 'Home',
  components: { PostList },
  setup() {
    const posts = ref([
      {
        title: 'ブログへようこそ',
        body: 'Lorem ipsum Lorem ipsumLorem ipsumLorem ipsumLorem ipsum',
        id: 1
      },
      { title: 'タイトル2', body: 'Lorem ipsum', id: 2 }
    ])

    return { posts }
  }
}
</script>
PostListをimportします。 componentsを設定します。 setup()の中でpostsを作成しているので、それをPostListで使えるようにtemplate内で記述します。
<template>
    <PostList :posts="posts" />
</template>
:postsでpostsを指定することでpostsを渡す準備ができます。

PostList.vue

<template>
  <div class="post-list">
    <div v-for="post in posts" :key="post.id">
      <h2>{{post.title}}</h2>
    </div>
  </div>
</template>
<script>

export default {
  props: ['posts'],
  setup(props) {
    console.log(props.posts); //postsはProxyオブジェクトの中の[[Target]]に格納されている
  }
}
</script>
propsでpostsを受け取ります。 setup()で使うときは、propsを引数として渡します。 postsはProxyオブジェクトの中の[[Target]]に格納されているので、props.postsとするとアクセスすることができます。

PostList.vueとSinglePost.vueをつなげる

まずはSinglePost.vueを作成します。 そして、PostList.vueを次のように修正します。

PostList.vue

<template>
  <div class="post-list">
    <div v-for="post in posts" :key="post.id">
      <SinglePost :post="post"/>
      <!-- <h2>{{post.title}}</h2> -->
    </div>
  </div>
</template>
SinglePostをv-forの中にいれます。 そうすることでpostsをループしてpostを渡すことができます。
<script>

import SinglePost from './SinglePost.vue'

export default {
  props: ['posts'],
  components: {SinglePost},
  setup(props) {
    console.log(props.posts); //postsはProxyオブジェクトの中の[[Target]]に格納されている
  }
}
</script>
SinglePostをimportします。 componentsを設定します。

SinglePost.vue

<template>
  <div class="post">
    <h3>{{ post.title }}</h3>
    <p>{{ snippet }}</p>
  </div>
</template>
propsでpostを設定したら、上のようにpostの中身にアクセスすることができます。
<script>
import { computed } from '@vue/runtime-core'

export default {
  props: ['post'],
  setup(props) {
    const snippet = computed(() => {
      return props.post.body.substring(0, 30) + '...'
    })

    return { snippet }
  }
}
</script>
propsでpostを設定します。 computedを使う場合は、忘れずにimportします。 そして、作成した変数は、忘れずにreturnします。

ライフサイクルフックを使う

ライフサイクルフックはsetup()の中でも今まで通りの記述でもどちらでも使うことができます。 setup()の中でも使う場合は、名前に接頭辞としてonがつく点だけが異なります。

PostList.vue

<script>

import { onMounted, onUnmounted, onUpdated } from '@vue/runtime-core'
import SinglePost from './SinglePost.vue'

export default {
  props: ['posts'],
  components: { SinglePost },
  setup(props) {
    // console.log(props.posts); //postsはProxyオブジェクトの中の[[Target]]に格納されている

    onMounted(() => console.log('component mounted'))
    onUnmounted(() => console.log('component unmounted'))
    onUpdated(() => console.log('component updated'))
  }
}
</script>
上のように、setup()の中で使う場合は、mounted()ならonMounted()とonがつきます。 忘れずに、vueからimportします。

Home.vue

<template>
  <div class="home">
    <h1>Home</h1>
    <PostList v-if="showPosts" :posts="posts" />
    <!-- onmountedとonUnmountedが反応する -->
    <button @click="showPosts = !showPosts">表示/非表示</button>
    <!-- onUpdatedが反応する -->
    <button @click="posts.pop()">削除</button>
  </div>
</template>
<script>
...
    const showPosts = ref(true)

    return { posts, showPosts }
</script>
onUnmounted()は、非表示になったときに反応します。 また、onUpdatedは対象が変更されたときに反応します。 変数を作成したら、忘れずにreturnしましょう。

Vue.js

Posted by devsakaso