【Vue3】<script setup>の学び

公開
更新
Photo by Minh Pham

・Vue3出たらしいけど何が変わったん?
・<script setup>とsetup()って何が違うん?
・provide/injectってなんぞ?
・suspenseってなんぞ?

という方々は是非ご一読ください。

本記事のテーマ

【Vue3】<script setup>を用いてTodoアプリを作ってみたので学んだことを共有

公式:SFC <script setup>

目次

・refやreactive
・propsとemit
・provide / inject
・suspense
・その他の変更点

筆者について


・未経験(花屋見習い)独学でフロントエンドエンジニアへ転職
・VueとTypeScriptのモダンフロントエンド環境でWebアプリ開発

前置きメッセージ


Vue3の<script setup>構文を用いて実際にTodoアプリを作ってみたのでそこから得た学びを共有します。
<script setup>はsetup関数のシンタックスシュガーです。
通常のsetup関数よりも簡潔に書けて可読性が向上します。
大きな変更点でいうと、returnが必要ないです。
また、propsやemitなどのオプションはdefinePropsやdefineEmitsといった新しい構文も存在します。

refやreactive


setup関数から使えた構文は同じように使用できます。
computedや、onMountedといったライフサイクルフックも同様です。
異なる点としてreturnが必要ないです。
script内で宣言するだけでtemplateに適用されます。
ただし、スコープがトップレベルのものだけです。
ライフサイクルフック、try、catchなどでブロックを作ると、適用されない。

👇 OK

<script lang="ts" setup>
import { reactive, ref } from 'vue'

const todos = ref<Todo[]>()
const todo = reactive<Todo>({
  title: todo.title,
  description: todo.description,
  status: todo.status
})
</script>


👇 NG

<script lang="ts" setup>
import { onMounted } from 'vue'
onMouted(async () => {  
  const todos = await fetchTodos();
}
  </script>


👇 ライフサイクルフックから初期値を取得したい時。

<script lang="ts" setup>
import { onMounted } from 'vue'

const todos = ref<Todo[]>()
onMouted(async () => {  
  todos.value = await fetchTodos();
}
</script>



propsとemit


変更点として大きく2点あります。
・TypeScriptの純粋なサポート
・新構文

👇 今までのprops

import { Proptype } from 'vue'
...
props: {
  todos: Array as Proptype<Array<Todo>>
}


👇 defineProps

type Props = {
  todo: Todo;
}
const props = defineProps<Props>();

// script内参照
props.todo

// template内参照
<div>{{ todo.name }}</div>


Propsのタイプを作ってdefinePropsにジェネリクスとして入れると自動で補完が効くようになります。
型定義で明示的に宣言して、definePropsで初期化のイメージです。

👇 プロパティをreiquired: falseにしたい場合は、オプショナルにすればOK

type Props = {
  todo?: Todo;
}


👇 デフォルト値はwithDefaults

const props = withDefaults(defineProps<Props>({
   todo: {
     id: 1
     name: '明日やること',
   } 
}));


👇 setup関数までのemit

<script lang="ts">
setup(props, ctx) {
  const clickDelete = (id: number) => {
    // setupじゃなければctxがthis
    ctx.emit('click-delete', id)
  }
}
</script>
....
<button @click="emit('click-delete', todo.id)">削除</button>


👇 defineEmits

type Emits = {
  (e: 'click-delete', id: number): void
};
const emit = defineEmits<Emits>()
...
<button @click="emit('click-delete', todo.id)">削除</button>


templateで使用する際に型が効くようになります。


definePropsとdefineEmitsどちらもimportなしでデフォルトで使えるようになってます。

provide / inject


これはsetup関数の時からありますが、今回使用したので復習がてら書きます。
provide / injectとはなんぞやという方はこちら
簡単に説明すると、親コンポーネントから孫コンポーネントへ子コンポーネントを経由せずにpropsを渡すことができます。
emitも同様です。

今回はtodoの状態を管理するためのファイル(store)を作り、そこからtodoの状態や状態を変更するための関数を呼び出しています。
それをApp.vueでprovideして各コンポーネントでinjectします。
storeと命名しておりますが今回はVuexは使用しておりません。

👇 provide / inject

// App.vue
import { provide } from 'vue'
import todoStore, { todoKey } from './store/todo'
provide(todoKey, todoStore)


// AsyncTodo.vue
import { inject } from 'vue'

const todoStore = inject(todoKey)
if (!todoStore) {
  throw new Error('todoStore is not provided')
}

<template>
  <ul>
    <todo-item
      v-for="todo in todoStore.state.todos"
      :key="todo.id"
      :todo="todo"
      @click-delete="todoStore.deleteTodo"
      @click-title="router.push(`/edit/${todo.id}`)"
    />
  </ul>
</template>



補足:todoStore

// store/todo/index.ts
const todoStore: TodoStore = {
  state: readonly(state),
  fetchTodos,
  fetchTodo,
  getTodo,
  addTodo,
  updateTodo,
  deleteTodo
}

export default todoStore

export const todoKey: InjectionKey<TodoStore> = Symbol('todo')


InjectionKey

Vue は Symbol を拡張したジェネリック型の InjectionKey インターフェイスを提供しています。これは Provider(プロバイダ)と Consumer(コンシューマ)の間で注入された値の型を同期するために使用できます:
vue公式


InjectionKeyを使うと注入側(inject)で型検査が効くようになります。

suspense


現段階では実験的な新機能とされているので、本番のアプリケーションでは使用しないでください。

個人的に一番気に入ってる機能です。
<script setup>構文ではトップレベルのawaitが使用できます。

<script lang="ts" setup>
const todos = await fetchTodos();
</script>


問題点

として、setupがPromiseでラップされます。
👇 こんなイメージ

async setup() {
  ...
  return {
    ...
  }
}

つまり宣言値がPromiseでラップされます。
そうなると、template側がそれを値として解決できず描画されません
そんな時に、suspenseを使いましょう。

suspenseの使用

👇 子コンポーネントでトップレベルのawait

// AsyncTodo.vue
<script lang="ts" setup>
import { inject } from 'vue'
import { todoKey } from '@/store/todo'
import TodoItem from '../components/TodoItem.vue'
import { useRouter } from 'vue-router'

const router = useRouter()

const todoStore = inject(todoKey)
if (!todoStore) {
  throw new Error('todoStore is not provided')
}

// トップレベルのawait
await todoStore.fetchTodos()
</script>

<template>
  <ul>
    <todo-item
      v-for="todo in todoStore.state.todos"
      :key="todo.id"
      :todo="todo"
      @click-delete="todoStore.deleteTodo"
      @click-title="router.push(`/edit/${todo.id}`)"
    />
  </ul>
</template>


👇 親コンポーネントでsuspenseを使用して、Promiseでラップされた値を描画

// Todos.vue
<script setup lang="ts">
import AsyncTodosVue from '@/components/AsyncTodos.vue'
</script>

<template>
  <h2>Todo一覧です。</h2>
  <suspense>
    <template #default>
      <AsyncTodosVue />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </suspense>
</template>


suspenseは二つのスロットをとります。
defaultfallbackです。
defaultには非同期コンポーネント、
fallbackにはdefaultの値が解決するまでの値
を入れましょう。

suspenseは一つの要素しかラップできません。
しかし、非同期コンポーネントが直径の子要素である必要はありません。
コンポーネントをラップした場合、そのコンポーネント配下の要素にもsuspenseが適用されます。

その他変更点


componentsオプションの廃止

書き忘れ常習犯の汚名返上。
importするだけで使えるようになります。

👇 これだけでOK

<script setup lang="ts">
import AsyncTodosVue from '@/components/AsyncTodos.vue'
</script>

<template>
  <h2>Todo一覧です。</h2>
  <suspense>
    <template #default>
      <AsyncTodosVue />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </suspense>
</template>


template側でのTypeScript補完

refやreactiveはもちろん、propsやemitも。
型にないことするとしっかり怒られます。

Vue-Router / Vuex

routerやstoreはuse関数で呼び出せるようになりました。

import { useRoute, useRouter } from 'vue-router'
import { useStore } from 'vuex' 

const router = useRouter()
const route = useRoute()
const store = useStore()


まとめ


Vue3 Compsition APIを使ってハンズオン形式でTODOアプリを作成
こちらのサイトを参考にTodoアプリを作ってみました。
リポジトリーパターンを採用されていて、知らない概念だったので勉強になりました。
Vueの新機能をふんだんに使い、体系的にまとめられている素晴らしい記事ですので、興味がある方は是非。
時々動かないコードがありますが、少し調べれば解決する程度でした。

同じ方のこちらの記事も参考にさせていただきました。
とてもわかりやすくまとめられています。
【Vue.js 3.2】<script setup>文がすごくすごい

輝良 / Kira

HTML, CSS, JavaScript, Vueを勉強して、未経験から独学でフロントエンドエンジニアへ転職。 実務ではTypeScriptとVueを使用。モダンフロントエンド技術が好き。 当サイトはNuxt3+TS+TailwindCSS+microCMSで構築。