Vue Routerをインストールしよう

Vue RouterはVueアプリケーションで複数のURLをアプリケーションに導入してView(ページ)を作成する場合に使います。

Vue Cliからインストールする場合

Vue CLI(コマンドラインインターフェイス)を使う場合はアプリを作成するときにVue Routerをインストールするにチェックをするだけで勝手にインストールしてくれます。

npmでインストールする場合

npm install vue-router@4

yarnでインストールする場合

yarn add vue-router@4

main.jsにRouterを追加

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

const app = createApp(App);

app.use(router);
app.mount("#app");

Routerディレクトリを作成

routerフォルダーをsrcフォルダの直下に作成します。その中にindex.jsを作成してrouterのコンフィグレーションを書いていきます。ファイル名はindex.jsでなくてもOKです。

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})
export default router

viewsディレクトリを作成

では、次に同じようにsrcディレクトリ直下にviewsをいう名前でフォルダーを作成しましょう。ここにページの枠組みとなるVueコンポーネントを作成していきます。

例でいうとAbout.vueとHome.vueになります。これでラウティングの設定が完了しました。あとはメニューバーなどを設置してユーザーを誘導させるようにすればよいですね。

router-linkとrouter-viewを設置

ではApp.vueに下記のようにコードを書き、ラウティングで指定したHome
とAboutにユーザーがアクセスできるようにしましょう。

その行先をクリックした際にViewを表示させるのが<router-view/>になります。

<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
  </div>
  <router-view/>
</template>

はい、上のようにメニューバーができて、クリックに応じてView(ページ)が変わりましたね。

これが基本的な使い方になります。

[Vue入門]Provide / Inject

Prop の過剰的な使用(Prop Drilling)

通常、親コンポーネントから子コンポーネントにデータを渡す必要がある場合、props を使用します。しかし、大きなコンポーネントツリーがあり、深くネスト化されたコンポーネントが遠い場合に親元から複数先のコンポーネントにデータを飛ばしたいシナリオを想定してください。

propsのやり方だと、親コンポーネントからつながるすべてのcomponentに同じ prop を渡さなければなりません:

<Footer> コンポーネントは親から受け取るの props を全く使わない場合でもデータの渡し役として、コンポーネントを記載する必要があります。それから<DeepChild> が<Footer>コンポーネントからこのデータにアクセスできるようにしてやっと<Root>コンポーネントのデータを受け取ることができるようになります。これでは、コードが煩雑になり、デバッグの作業も面倒になりますね。

このPropsを何階層も下に投げる作業を省くには provide と inject を使うことで解決できます。親コンポーネントは、そのすべての子コンポーネントに対して 依存関係を提供するプロバイダー (dependency provider) として機能することができます。子ツリー内のどのコンポーネントも、その深さに関係なく、親チェーン内の上位コンポーネントが提供する依存性を注入 (inject) することができます。

Provide

親コンポーネントから子コンポーネントにデータを提供するには provide() 関数を使います:

<script setup>
import { provide } from 'vue'

provide(/* key */ 'message', /* value */ 'hello!')
</script>

<script setup> を使わない場合、setup() 内で provide() が同期的に呼び出されていることを確認してください:

import { provide } from 'vue'

export default {
  setup() {
    provide(/* key */ 'message', /* value */ 'hello!')
  }
}

provide() 関数は 2 つの引数を受け付けます。第 1 引数はインジェクションキーと呼ばれ、文字列または Symbol となります。provideで使用するデータを投げるために使うニックネームのようなものですね。

このインジェクションキーは、子のコンポーネントが、インジェクション(注入)に必要な値を探すのに使われます。1 つのコンポーネントが異なる値を提供するために、異なるインジェクションキーで provide() を複数回呼び出すことができます。

第 2 引数は提供される値です。この値は refs のようなリアクティブな状態を含む、任意の型にすることができます:

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

リアクティブな値を提供することで、提供された値を使用する子孫コンポーネントが、プロバイダーコンポーネントとのリアクティブな接続を確立することができます。

アプリケーションレベルの Provide

コンポーネント内だけでなく、アプリケーションレベルでデータを提供することも可能です:

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* key */ 'message', /* value */ 'hello!')

アプリケーションレベルの Provide は、アプリケーションでレンダリングされるすべてのコンポーネントで利用可能です。これは特にプラグインを書くときに便利です。プラグインは通常、コンポーネントを使ってデータを提供することができないからです。

Inject

親コンポーネントが提供するデータを注入するには inject() 関数を使用します:

<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>

提供された値が ref である場合、そのまま注入され、自動的にアンラップされることはありません。これにより、インジェクターコンポーネントはプロバイダーコンポーネントとのリアクティビティの接続を保持することができます。

繰り返しますが、もし <script setup> を使用しないのであれば、inject() は setup() の内部でのみ同期的に呼び出す必要があります:

import { inject } from 'vue'

export default {
  setup() {
    const message = inject('message')
    return { message }
  }
}

インジェクションのデフォルト値

デフォルトでは、inject は注入されるキーが親チェーンのどこかで提供されることを想定しています。キーが提供されていない場合、実行時が出ます。

インジェクトされたプロパティをオプションのプロバイダーで動作させたい場合は、props と同様にデフォルト値を宣言する必要があります:

// もし "message" にマッチするデータがなかった場合は、
// `value` は "default value" になります
const value = inject('message', 'default value')

場合によっては、関数を呼び出したり、新しいクラスをインスタンス化したりして、デフォルト値を作成する必要があるかもしれません。オプションの値が使用されないケースで不要な計算や副作用を避けるために、デフォルト値を作成するためのファクトリー関数を使用することができます:

const value = inject('key', () => new ExpensiveClass())

リアクティビティと共に利用する

リアクティブな値を provide / inject する場合、可能な限り、リアクティブな状態への変更を provider の内部で維持することが推奨されます。これは、提供されるステートとその可能な変更が同じコンポーネントに配置されることを保証し、将来のメンテナンスをより容易にするためです。

インジェクターコンポーネントからデータを更新する必要がある場合があります。そのような場合は、状態の変更を担当する関数を使うことをおすすめします:

<!-- プロバイダーコンポーネント内部 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>
<!-- インジェクターコンポーネント内部 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

最後に、provide を通して渡されたデータが注入されたコンポーネントによって変更されないようにしたい場合は、提供された値を readonly() でラップすることができます。

<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

シンボルキーと共に利用する

今までの例では、文字列のインジェクションキーを使っていました。もしあなたが多くの依存関係を提供するプロバイダーを持つ大規模なアプリケーションで作業していたり、他の開発者が使用する予定のコンポーネントを作成している場合は、名前の重複を避けるためにシンボルインジェクションキーを使用するのがベストです。

シンボルは専用のファイルに書き出しておくことをおすすめします:

// keys.js
export const myInjectionKey = Symbol()
// プロバイダーコンポーネント内
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, {
  /* 提供するデータ */
})
// インジェクターコンポーネント内
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

[Vue入門] スロット

このページは、コンポーネントの基本をすでに読んでいることを前提としています。コンポーネントを初めて使用する場合は、最初にそれをお読みください。

スロットコンテンツとその出力先

コンポーネントはpropsを受け入れることができることを学びましたね。propは色々なタイプのJavaScriptのデータを格納することができます。しかし、テンプレートコンテンツはどうでしょうか?再利用可能なコンポーネントを作成するうえで、親のコンポーネントから自在に意図した場所にデータをレンダーしたい場合にスロットが役に立ちます。

たとえば、次の<FancyButton>ような使用法をサポートするコンポーネントがある場合があるとします。

<FancyButton>
  Click me! <!-- slot content -->
</FancyButton>

のテンプレートは<FancyButton>次のようになります。

<button class="fancy-btn">
  <slot></slot> <!-- slot outlet -->
</button>

この<slot>エレメントは、親が提供するスロットコンテンツをレンダリングする場所を示すスロットアウトレットです。

いわば枠組みを作ってあげてその中(スロット)に意図したデータを投げてあげるわけですね。

そして、最終的にレンダリングされたDOMはこのようになります。:

<button class="fancy-btn">
  Click me!
</button>

スロットを使用すると、<FancyButton>は外側(およびその派手なスタイリング)のレンダリングを担当し、<button>内側のコンテンツは親コンポーネントによって提供されます。

スロットを理解するもう1つの方法として、スロットをJavaScript関数と比較することができます。

// parent component passing slot content
FancyButton('Click me!')

// FancyButton renders slot content in its own template
function FancyButton(slotContent) {
  return (
    `<button class="fancy-btn">
      ${slotContent}
    </button>`
  )
}

スロットコンテンツはテキストだけに限定されません。有効なテンプレートコンテンツであれば何でもOKです。たとえば、複数のHTMLエレメント、または他のコンポーネントを渡すことができます。

<FancyButton>
  <span style="color:red">Click me!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

スロットを使用することで、<FancyButton>をより柔軟で再利用可能なコンポーネントとして使用できるようになります。

Vueコンポーネントのスロットメカニズムは、ネイティブのWebコンポーネント<slot>要素で記載されていますが、後で説明する他の機能も備えています。

レンダリングスコープ

スロットコンテンツは、親で定義されているため、親コンポーネントのデータスコープにアクセスできます。例えば:

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

ここでは、両方の{{ message }}補間で同じコンテンツがレンダリングされます。

スロットコンテンツは、子コンポーネントのデータにアクセスできません。原則として、次の点に注意してください。

親テンプレートのすべてが親スコープでコンパイルされ、子テンプレートのすべてが子スコープでコンパイルされます。

スロット内容が指定されていない場合の初期値

コンテンツが提供されていない場合にのみレンダリングされるように、スロットのフォールバック(つまりデフォルト)コンテンツを指定できるという便利な機能があります。たとえば、<SubmitButton>コンポーネントでは次のようになります。

<button type="submit">
  <slot></slot>
</button>

<button>親コンポーネントでスロットコンテンツを提供しなかった場合は、「Submit」というテキストをの中に表示したい場合を想定します。<slot>タグの間にフォールバックコンテンツ(スロット内容が指定されていない場合の初期値)を記載することで初期値を「Submit」にできます。

<button type="submit">
  <slot>
    Submit <!-- fallback content -->
  </slot>
</button>

親コンポーネントで使用する場合<SubmitButton>、スロットにコンテンツを提供なかった場合。

<SubmitButton />

これにより、フォールバックコンテンツ(デフォルト値)「Submit」がレンダリングされます。

<button type="submit">Submit</button>

ただし、スロットコンテンツを提供する場合:

<SubmitButton>Save</SubmitButton>

次に、提供されたコンテンツが代わりにレンダリングされます。

<button type="submit">Save</button>

複数のスロットがある場合

1つのコンポーネントに複数のスロットがあると便利な場合があります。次のテンプレート<BaseLayout>を使用するコンポーネントでは、次のようになります。

<div class="container">
  <header>
    <!-- We want header content here -->
  </header>
  <main>
    <!-- We want main content here -->
  </main>
  <footer>
    <!-- We want footer content here -->
  </footer>
</div>

このような場合、<slot>エレメントには特別な属性として、nameというものがあります。これを使用して、コンテンツをレンダリングする場所を指定できるようになり、さまざまなスロットに意図したIDを割り当てることができます。

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

例を見てわかるようにスロットネームのないデフォルト用のの<slot>があります。name slotを使用する親コンポーネント<BaseLayout>では、それぞれが異なるスロット出力先をターゲットとする複数のスロット出力先に提供する方法が必要です。ここで名前付きスロットの出番です。

名前付きスロットを渡すには、ディレクティブで<template>要素を使用してから、v-slotを使ってスロットの名前を引数として次のように渡す必要があります。

<BaseLayout>
  <template v-slot:header>
    <!-- content for the header slot -->
  </template>
</BaseLayout>

v-slotは省略して#記載できるので、<template v-slot:header>から<template #header>と記載することができます。

3つのスロットすべてのコンテンツを<BaseLayout>省略構文を使用して渡す場合は次のとおりになります。

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

コンポーネントがデフォルトスロットと名前付きスロットの両方を受け入れる場合、すべての上階層の<template>ではないノード(HTML)は自動的にデフォルトスロットのコンテンツとして扱われます。したがって、上記は次のように書くこともできます。

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- implicit default slot -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

これで、エレメント内のすべて<template>が対応するスロットに渡されます。最終的にレンダリングされるHTMLは次のようになります。

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

繰り返しになりますが、JavaScript関数の例を使用すると、名前付きスロットをよりよく理解するのに役立つ場合と思います。

// passing multiple slot fragments with different names
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> renders them in different places
function BaseLayout(slots) {
  return (
    `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
  )
}

ダイナミックなスロット名

動的ディレクティブ引数はv-slotでも使用可能で、動的スロット名の定義をすることができます。

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- with shorthand -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

式は動的ディレクティブ引数の構文制約に従うことに注意してください。

スコープスロット

レンダリングスコープで説明したように、スロットコンテンツは子コンポーネントの状態にアクセスできません。

ただし、スロットのコンテンツが親スコープと子スコープの両方のデータを利用できると便利な場合があります。これを実現するには、子がデータをレンダリングするときにスロットにデータを渡す方法が必要になります。

これは、Propsをコンポーネントに渡すのと同じように、属性をスロットアウトレットに渡すことがで実現可能になります。:

<!-- <MyComponent> template -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

単一のデフォルトスロットを使用する場合と名前付きスロットを使用する場合では、スロットプロップの受信の仕方が少し異なります。v-slot子コンポーネントタグを直接使用して、最初に単一のデフォルトスロットを使用してPropsを受け取る方法を記載します。

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

子によってスロットに渡されるpropsは、対応するv-slotディレクティブの値として使用でき、スロット内の式からアクセスできます。

スコープスロットは、子コンポーネントに渡される関数と考えることができます。次に、子コンポーネントはそれを呼び出し、Propsを引数として渡します。

MyComponent({
  // passing the default slot, but as a function
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'hello'
  return (
    `<div>${
      // call the slot function with props!
      slots.default({ text: greetingMessage, count: 1 })
    }</div>`
  )
}

実際、これはスコープ付きスロットのコンパイル方法、および手動レンダリング関数でスコープ付きスロットを使用する方法に非常に近いものです。

スロット機能のシグネチャとv-slot=”slotProps”がどのように一致するかに注意してください。関数の引数と同じように、v-slot内で次の場所で各データを使用できます。

<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

名前付きスコープスロット

名前付きスコープスロットも同様に機能します。スロットのPropsは、v-slotディレクティブの値としてアクセスできますv-slot:name="slotProps"。省略した記載方法を使用すると、次のようになります。

<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

名前付きスロットにPropsを渡す:

<slot name="header" message="hello"></slot>

nameスロットでnameというストリングが使用されるため、nameというプロップは制限され、Propsには含まれないことに注意してください。そのため、結果headerPropsは{ message: ‘hello’ }になります。

ファンシーリストの例

スコープ付きスロットの良い使い方は何か疑問に思われるかもしれません。次に例を記載します。<FancyList>アイテムのリストをレンダリングするコンポーネントを想像してみてください。リモートデータの読み込み、データを使用したリストの表示、さらにはページネーションや無限スクロールなどの高度な機能のロジックをカプセル化できます。ただし、各アイテムの外観に柔軟性を持たせ、各アイテムのスタイルをそれを使用する親コンポーネントに任せる必要があります。したがって、望ましい使用法は次のようになります。

<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

<FancyList>内部では、異なるアイテムデータを使用して同じものを複数回レンダリングできます(オブジェクトをスロットプロップとして渡すために<slot>v-bindを使用していることに注意してください)。

<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

レンダーレスコンポーネント

上で説明した<FancyList>ユースケースは、再利用可能なロジック(データのフェッチ、ページネーションなど)とビジュアル出力の両方をカプセル化し、スコープ付きスロットを介してビジュアル出力の一部をコンシューマーコンポーネントとして使用したものになります。

この概念をもう少しひねると、ロジックのみをカプセル化し、それ自体では何もレンダリングしないコンポーネントを作ることができます。ビジュアル出力は、スコープスロットを備えたコンシューマーコンポーネントに完全に任されることになります。このタイプのコンポーネントをレンダーレスコンポーネントと呼びます。

レンダーレスコンポーネントの例としては、現在のマウス位置を追跡するロジックをカプセル化したものがあります。

<MouseTracker v-slot="{ x, y }">
  Mouse is at: {{ x }}, {{ y }}
</MouseTracker>

興味深いパターンですが、レンダリングレスコンポーネントで実現できることのほとんどは、余分なコンポーネントのネストのオーバーヘッドを発生させることなく、CompositionAPIを使用してより効率的な方法で実現できます。後で、Composableと同じマウス追跡機能を実装する方法を説明します。

まとめとしてスコープスロットは、<FancyList>の例のように、ロジックの格納化とビジュアル出力の作成の両方が必要な場合で役立つことが分かりますね。

[Vue入門] フォールスルー属性

このページは、コンポーネントの基礎をすでに読んでいることを想定して説明しています。初めてコンポーネントに触れる方は、まずそちらをお読みください。

属性の継承

“フォールスルー属性”とは、あるコンポーネントに渡されたものの、受け取ったコンポーネントの props や emits で明確に宣言されていない属性、または v-on イベントリスナーを指します。よくある例としては、classstyleid 属性などがあります。

コンポーネントが単一のルート要素をレンダリングする時、フォールスルー属性は自動的にルート要素の属性に追加されます。例えば、次のようなテンプレートを持つ <MyButton> コンポーネントがあったとします:

<!-- <MyButton> のテンプレート -->
<button>click me</button>

そして、このコンポーネントを使う親が以下です:

<MyButton class="large" />

最終的に DOM は以下のようにレンダリングされます:

<button class="large">click me</button>

class と style のマージ

もし、子コンポーネントのルート要素にすでに class や style 属性がある場合は、親から継承された class や style の値にマージされます。先ほどの例の <MyButton> のテンプレートを次のように変更するとします:

<!-- <MyButton> の テンプレート -->
<button class="btn">click me</button>

そうすると、最終的にレンダリングされる DOM は、こうなります:

<button class="btn large">click me</button>

v-on リスナーの継承

同じルールが v-on イベントリスナーにも適用されます:

<MyButton @click="onClick" />

click リスナーは <MyButton> のルート要素、つまりネイティブの <button> 要素に追加されます。ネイティブの <button> がクリックされた時、親コンポーネントの onClick メソッドがトリガーされます。もし、ネイティブの <button> が既に v-on でバインドされた click リスナーを持っている場合、両方のリスナーがトリガーされます。

ネストされたコンポーネントの継承

あるコンポーネントが他の 1 つのコンポーネントをルートノードとしてレンダリングする場合を考えてみましょう。例として、<MyButton> をルートとして <BaseButton> をレンダリングするようにリファクタリングしました:

<!-- シンプルに他の 1 つのコンポーネントをレンダリングする <MyButton/> のテンプレート -->
<BaseButton />

この時、<MyButton> が受け取ったフォールスルー属性は、自動的に <BaseButton> に転送されます。

以下の点に注意してください:

  1. 転送された属性には、<MyButton> が props として宣言した属性や、宣言したイベントの v-on リスナーは含まれません。言い換えると、宣言した props とリスナーは <MyButton> によって “消費” されています。
  2. 転送された属性は、 <BaseButton> が宣言していれば、 props として受け取ることができます。

属性の継承の無効化

コンポーネントに自動的な属性の継承をさせたくない場合は、コンポーネントのオプションで inheritAttrs: false を設定することができます。

<script setup> を使用するなら、このオプションは別の通常の <script> ブロックを使って宣言する必要があります:

<script>
// 通常の <script> でオプションを宣言
export default {
  inheritAttrs: false
}
</script>

<script setup>
// ロジックのセットアップ
</script>

属性の継承を無効にする一般的なシナリオは、ルートノード以外の要素に属性を適用する必要がある場合です。 inheritAttrs オプションを false に設定することで、フォールスルー属性を適用する場所を完全に制御することができます。

これらのフォールスルー属性は、テンプレート式で $attrs として直接アクセスすることができます:

<span>Fallthrough attributes: {{ $attrs }}</span>

$attrs オブジェクトには、コンポーネントの props や emits オプションで宣言されていないすべての属性 (例えば classstylev-on リスナーなど) が含まれます。

備考:

  • props とは異なり、フォールスルー属性は JavaScript では元のケーシングを保持します。したがって、 foo-bar のような属性は $attrs['foo-bar'] としてアクセスされる必要があります。
  • @click のような v-on イベントリスナーは、オブジェクトで $attrs.onClick という関数として公開されます。

前のセクションで紹介した <MyButton> コンポーネントの例では、スタイリングのために実際の <button> 要素を <div> でラップする必要がある場合があります:

<div class="btn-wrapper">
  <button class="btn">click me</button>
</div>

class や v-on リスナーなどのすべてのフォールスルー属性を、外側の <div> ではなく、内側の <button> に適用されるようにしたいです。これは、 inheritAttrs: false と v-bind="$attrs" で実現できます:

<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">click me</button>
</div>

引数なしの v-bind はオブジェクトのすべてのプロパティをターゲット要素の属性としてバインドすることを覚えておきましょう。

複数のルートノードでの属性継承

ルートノードが 1 つのコンポーネントと異なり、複数のルートノードを持つコンポーネントは、自動的に属性をフォールスルーするふるまいがありません。 $attrs が明示的にバインドされていない場合は、実行時に警告が出ます。

<CustomLayout id="custom-layout" @click="changeValue" />

もし <CustomLayout> が以下のようなマルチルートのテンプレートを持っている場合、 Vue はどこにフォールスルー属性を適用すればよいか分からないため、警告されます:

<header>...</header>
<main>...</main>
<footer>...</footer>

警告は $attrs が明示的にバインドされている場合は抑制されます:

<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>

JavaScript 内でフォールスルー属性にアクセスする

必要であれば、<script setup> 内で useAttrs() API を使用してコンポーネントのフォールスルー属性にアクセスすることができます:

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

もし <script setup> を使用していない場合、 attrs は setup() コンテキストのプロパティとして公開されます:

export default {
  setup(props, ctx) {
    // フォールスルー属性が ctx.attrs として公開される
    console.log(ctx.attrs)
  }
}

ここで attrs オブジェクトは常に最新のフォールスルー属性を反映していますが、リアクティブではないことに注意してください(パフォーマンス上の理由です)。ウォッチャーを使ってその変更を監視することはできません。リアクティビティが必要であれば、 prop を使ってください。または、 onUpdated() を使用して、更新されるたびに最新の attrs による副作用を実行することもできます。

[Vue入門] Emitの使い方

このページは、コンポーネントの基本をすでに読んでいることを前提としています。コンポーネントを初めて使用する場合は、最初にそれをお読みください。

イベントの送信とリスニング

$emitを使用してコンポーネントからv-onでバインドし、テンプレートにカスタムイベントを直接設定することができます。

<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

次に、親はv-onを使用して子のcomponentから発信されたイベントを聞くことができます。

<MyComponent @some-event="callback" />

修飾子は、.onceコンポーネントイベントリスナーでもサポートされています。

<MyComponent @some-event.once="callback" />

コンポーネントやPropsと同様に、イベント名はケースの自動変換を提供します。キャメルケースイベントを使用しても、親のケバブケースのリスナーを使用してそれをどちらでもVueで同じものとして扱われることに注意してください。Propsのケーシングと同様に、テンプレートではケバブケースのイベントリスナーを使用することをお勧めします。

ヒント

ネイティブDOMイベントとは異なり、コンポーネントが発行するイベントはバブルしません。直接の子コンポーネントによって実行されたイベントのみを聞くことができます。

イベント引数

イベントで特定の値を実行すると便利な場合があります。たとえば<BlogPost>、テキストをどれだけ拡大するかをコンポーネントが担当するようにしたい場合があります。$emitそのような場合、この値を提供するために追加の引数を渡すことができます。

<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>

次に、親でイベントをリッスンするときに、インライン関数をリスナーとして使用できます。これにより、イベント引数にアクセスできます。

<MyButton @increase-by="(n) => count += n" />

または、イベントハンドラーがメソッドの場合:

<MyButton @increase-by="increaseCount" />

次に、その値がそのメソッドの最初のパラメーターとして渡されます。

function increaseCount(n) {
  count.value += n
}

ヒント

イベント名の後に渡されたすべての追加の引数は$emit()、リスナーに転送されます。たとえば$emit('foo', 1, 2, 3)、listener関数では3つの引数を受け取ります。

Emitイベントの宣言

defineEmits()放出されたイベントは、マクロを介してコンポーネントで明示的に宣言できます。

<script setup>
defineEmits(['inFocus', 'submit'])
</script>

で使用した$emitメソッドは、コンポーネント<template>のセクション内ではアクセスできませんが、代わりに使用できる同等の関数を返します。<script setup>defineEmits()

<script setup>
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
  emit('submit')
}
</script>

defineEmits()マクロは関数内では使用できません<script setup>。上記の例のように、マクロ内に直接配置する必要があります。

setupの代わりに明示的な関数を使用している場合は、オプション<script setup>を使用してイベントを宣言する必要があります。

export default {
  emits: ['inFocus', 'submit'],
  setup(props, ctx) {
    ctx.emit('submit')
  }
}

setup()をしようした場合はコンテキストは他のプロパティと同様です。

export default {
  emits: ['inFocus', 'submit'],
  setup(props, { emit }) {
    emit('submit')
  }
}

このemitsオプションは、オブジェクト構文もサポートしています。これにより、実行されたイベントのペイロードの実行時検証を実行できます。

<script setup>
const emit = defineEmits({
  submit(payload) {
    // return `true` or `false` to indicate
    // validation pass / fail
  }
})
</script>

TypeScriptをで使用している場合は<script setup>、純粋な型アノテーションを使用して発行されたイベントを宣言することもできます。

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>

詳細:コンポーネントエミットの入力 

オプションですが、コンポーネントがどのように機能するかをより適切に文書化するために、発行されたすべてのイベントを定義することをお勧めします。また、Vueが既知のリスナーをフォールスルー属性から除外することもできます。

ヒント

オプションでネイティブイベント(たとえばclick)が定義されているemits場合、リスナーはコンポーネントから送信されたイベントのみをリッスンし、ネイティブイベントに応答しなくなります。

イベントの検証

プロップタイプの検証と同様に、発行されたイベントは、配列構文ではなくオブジェクト構文で定義されている場合に検証できます。

検証を追加するために、イベントには、呼び出しに渡された引数を受け取り、イベントが有効かどうかを示すブール値を返す関数が割り当てられます。emit

<script setup>
const emit = defineEmits({
  // No validation
  click: null,

  // Validate submit event
  submit: ({ email, password }) => {
    if (email && password) {
      return true
    } else {
      console.warn('Invalid submit event payload!')
      return false
    }
  }
})

function submitForm(email, password) {
  emit('submit', { email, password })
}
</script>

v-modelとEmitの使い方

カスタムイベントを使用して、v-modelと連携できるカスタムインプットを作ることもできます。

<input v-model="searchText" />

上記のコードは下のものと同じことを意味します。

<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

コンポーネントで使用する場合、少し違う動きが発生し、下記のようになります。

<CustomInput
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>

ただし、これを実際に機能させるには、<input>のコンポーネントの内部で次のことを行う必要があります。

  • value属性をmodelValueプロップにバインドします
  • で、新しい値でイベントを発行inputしますupdate:modelValue

これが実際の動作です。

<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

これv-modelで、このコンポーネントで完全に機能するはずです。

<CustomInput v-model="searchText" />

遊び場で試してみてください

v-modelこのコンポーネント内に実装する別の方法はcomputed、ゲッターとセッターの両方で書き込み可能なプロパティを使用することです。メソッドはプロパティをget返し、メソッドは対応するイベントを発行する必要があります。modelValueset

<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit('update:modelValue', value)
  }
})
</script>

<template>
  <input v-model="value" />
</template>

v-model引数

デフォルトでv-modelは、コンポーネントmodelValueで小道具およびupdate:modelValueイベントとして使用されます。引数を渡してこれらの名前を変更できますv-model

<MyComponent v-model:title="bookTitle" />

この場合、子コンポーネントはtitle小道具を期待しupdate:title、親の値を更新するイベントを発行する必要があります。

<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

遊び場で試してみてください

複数v-modelのバインディング

v-model以前に引数で学習した特定の小道具とイベントをターゲットにする機能を活用することで、単一のコンポーネントインスタンスに複数のvモデルバインディングを作成できるようになりました。

各v-modelは、コンポーネントに追加のオプションを必要とせずに、異なるプロップに同期します。

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

遊び場で試してみてください

v-model修飾子(モディファイヤ)

フォーム入力バインディングについて学習していたときに、.trim.number.lazyのような修飾子v-modelが組み込まれていることを確認しました。ただし、場合によっては、独自のカスタム修飾子を追加することもできます。

バインディングcapitalizeによって提供される文字列の最初の文字を大文字にするカスタム修飾子の例を作成しましょう。v-model

<MyComponent v-model.capitalize="myText" />

コンポーネントに追加されたモディファイヤは、プロップv-modelを介してコンポーネントに提供されます。次の例では、デフォルトで空のオブジェクトにmodelModifiersなるpropを含むコンポーネントを作成しました。

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

defineEmits(['update:modelValue'])

console.log(props.modelModifiers) // { capitalize: true }
</script>

<template>
  <input
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

コンポーネントのmodelModifierspropにが含まれていてcapitalize、その値がtrueであることに注意してください。これは、v-modelバインディングに設定されているためv-model.capitalize="myText"です。

プロップが設定されたので、modelModifiersオブジェクトキーを確認し、出力された値を変更するハンドラーを作成できます。<input />以下のコードでは、要素がイベントを発生させるたびに文字列を大文字にしますinput

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

遊び場で試してみてください

v-model引数と修飾子の両方を持つバインディングの場合、生成されるプロップ名はarg + "Modifiers"になります。例えば:

<MyComponent v-model:title.capitalize="myText">

対応する宣言は次のようになります。

const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])

console.log(props.titleModifiers) // { capitalize: true }

[Vue入門] PropsとdefineProps 

このページは、すでにコンポーネントの基礎を読んでいることを前提にしています。初めてコンポーネントに触れる方は、まずこちらをお読みください。

プロパティの宣言

Vue のコンポーネントでは、明示的な props (プロパティ) の宣言が必要です。これにより Vue は、外部からコンポーネントに渡された props を fallthrough 属性 (次のセクションで説明します) として扱うべきかを知ることができます。

プロパティは、以下のように props オプションを使って宣言します:

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

//<script setup>を使わないやり方↓
export default {
  props: ['foo'],
  created() {
    // props は `this` 上で公開されます。
    console.log(this.foo)
  }
}

プロパティの宣言には、文字列の配列に加え、オブジェクト構文を用いることもできます:

// in <script setup>
defineProps({
  title: String,
  likes: Number
})
export default {
  props: {
    title: String,
    likes: Number
  }
}

オブジェクト宣言の構文に含める各プロパティについて、キーにはプロパティの名前、値には目的の型のコンストラクター関数を指定します。

これは自分のコンポーネントを文書化するのに役立ちます。また、誤った型を渡した時にブラウザーのコンソールに警告が表示されるようになり、コンポーネントを利用する他の開発者のためにもなります。このページの後半では、プロパティのバリデーションについて詳しく説明します。

コンポーネントのプロパティの型付けも合わせて参照してください。

プロパティ渡しの詳細

プロパティ名での大文字・小文字の使い分け

長いプロパティ名は、camelCase (キャメルケース) で宣言します。そうすると、プロパティのキーとして使うときに引用符を使わなくて済みます。camelCase は JavaScript の有効な識別子であるため、以下のようにテンプレート式で直接参照することができます:

defineProps({
  greetingMessage: String
})

//<script setup>を使わないやり方↓
export default {
  props: {
    greetingMessage: String
  }
}
<span>{{ greetingMessage }}</span>

技術的には、子コンポーネントにプロパティを渡すときにも camelCase を用いることができます (ただし DOM テンプレート内を除く)。しかし、常に kebab-case (ケバブケース) を用いて HTML の属性に揃える、以下のような表記が慣例となっています:

<MyComponent greeting-message="hello" />

コンポーネントのタグには、可能な限り PascalCase を用いることが推奨されます。これは Vue コンポーネントとネイティブ要素の区別が付き、テンプレートの可読性が高まるためです。しかし、プロパティを渡すときに camelCase を用いることには、それほど実用的なメリットがありません。そのため、Vue では各言語の規約に従うことが推奨されます。

静的なプロパティと動的なプロパティ

ここまでで、静的な値として渡すプロパティを見てきました。例:

<BlogPost title="My journey with Vue" />

v-bind またはそのショートカットである : を使って、プロパティを動的に割り当てる例も見てきました。例:

<!-- 変数の値を動的に代入 -->
<BlogPost :title="post.title" />

<!-- 複雑な式の値を動的に代入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />

いろいろな種類の値を渡す

上の 2 つは、たまたま文字列の値を渡す例ですが、プロパティには どんな 種類の値も渡すことができます。

数値

<!-- `42` は静的な値ですが、これが文字列ではなく JavaScript の        -->
<!-- 式であることを Vue に伝えるため、v-bind を用いる必要があります。 -->
<BlogPost :likes="42" />

<!-- 変数の値に動的に代入します。 -->
<BlogPost :likes="post.likes" />

ブール値

<!-- 値なしでプロパティを指定すると、暗黙で `true` を指定したことになります。 -->
<BlogPost is-published />

<!-- `false` は静的な値ですが、これが文字列ではなく JavaScript の     -->
<!-- 式であることを Vue に伝えるため、v-bind を用いる必要があります。 -->
<BlogPost :is-published="false" />

<!-- 変数の値に動的に代入します。 -->
<BlogPost :is-published="post.isPublished" />

配列

<!-- 静的な配列でも、これが文字列ではなく JavaScript の         -->
<!-- 式であることを Vue に伝えるため、v-bind を用いる必要があります。 -->
<BlogPost :comment-ids="[234, 266, 273]" />

<!-- 変数の値に動的に代入します。 -->
<BlogPost :comment-ids="post.commentIds" />

オブジェクト

<!-- 静的なオブジェクトでも、これが文字列ではなく JavaScript の -->
<!-- 式であることを Vue に伝えるため、v-bind を用いる必要があります。 -->
<BlogPost
  :author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
 />

<!-- 変数の値に動的に代入します。 -->
<BlogPost :author="post.author" />

オブジェクトを利用した複数のプロパティのバインディング

オブジェクトに含まれるすべてのプロパティを props として渡したい場合には、引数なしの v-bind を使用します (:プロパティ名 の代わりに v-bind)。例えば、以下のような post オブジェクトがあるとします:

const post = {
  id: 1,
  title: 'My Journey with Vue'
}

//<script setup>を使わないやり方↓
export default {
  data() {
    return {
      post: {
        id: 1,
        title: 'My Journey with Vue'
      }
    }
  }
}

このとき、テンプレートでは以下を用いることができます:

<BlogPost v-bind="post" />

これは以下の表記と等価です:

<BlogPost :id="post.id" :title="post.title" />

一方向のデータフロー

すべてのプロパティでは、子のプロパティと親のプロパティとの間に一方向バインディングが形成されます。親のプロパティが更新されたときには子にも流れますが、その逆はありません。これにより、親の状態が誤って子コンポーネントによって変更されてアプリのデータフローが把握しにくくなる、といった事態が防がれます。

さらに、親コンポーネントが更新されるたびに、子コンポーネント内のすべてのプロパティは最新の値に更新されます。そのため、子コンポーネント内でプロパティの変更を試みてはいけません。もし試みると、Vue がコンソールで警告を発します:

const props = defineProps(['foo'])

// ❌ warning, props are readonly!
props.foo = 'bar'


//<script setup>を使わないやり方↓
export default {
  props: ['foo'],
  created() {
    // ❌ 警告、プロパティは読み取り専用です!
    this.foo = 'bar'
  }
}

通常、プロパティを変更したい状況には以下の 2 つがあります:

  • プロパティは初期値を渡すために用いて、それ以降、子コンポーネントではローカルのデータプロパティとして利用したい。 この場合、以下のようにローカルのデータプロパティを定義して、その初期値にプロパティを使用するのが最も適切です:export default { props: ['initialCounter'], data() { return { // this.initialCounter は counter の初期値を指定するためだけに // 使われ、今後発生するプロパティの更新からは切り離されます。 counter: this.initialCounter } } }
const props = defineProps(['initialCounter'])

// counter only uses props.initialCounter as the initial value;
// it is disconnected from future prop updates.
const counter = ref(props.initialCounter)
  • プロパティを、変換が必要なそのままの値として渡したい。 この場合、以下のような算出プロパティを定義して、その中でプロパティの値を利用するのが最も適切です:export default { props: ['size'], computed: { // プロパティが変更されると自動的に更新される算出プロパティ normalizedSize() { return this.size.trim().toLowerCase() } } }
const props = defineProps(['size'])

// computed property that auto-updates when the prop changes
const normalizedSize = computed(() => props.size.trim().toLowerCase())

オブジェクト/配列のプロップを変更する

オブジェクトや配列をプロパティとして渡した場合、子コンポーネントがプロパティのバインディングを変更することはできませんが、オブジェクトや配列のネストされたプロパティを変更することは可能です。これは、JavaScript ではオブジェクトや配列が参照渡しであり、Vue がそのような変更を防ぐのにかかるコストが現実的でないためです。

このような変更の主な欠点は、親コンポーネントにとって明瞭でない方法で子コンポーネントが親の状態に影響を与えることを許してしまい、後からデータの流れを見極めるのが難しくなる可能性があることです。親と子を密に結合させる設計でない限り、ベストプラクティスとしてはそのような変更を避けるべきです。ほとんどの場合、子コンポーネントはイベントを発行して、変更を親コンポーネントに実行してもらう必要があります。

プロパティのバリデーション

先ほど見た型のように、コンポーネントではプロパティに対する要件を指定することができます。要件が合わないと、Vue がブラウザーの JavaScript コンソールで警告を発します。他の人に使ってもらうことを想定したコンポーネントを開発する場合、これはとても便利です。

プロパティのバリデーションを指定するには、文字列の配列の代わりに props オプションを用いて、バリデーションの要件を持たせたオブジェクトを指定します。例:

defineProps({
  // Basic type check
  //  (`null` and `undefined` values will allow any type)
  propA: Number,
  // Multiple possible types
  propB: [String, Number],
  // Required string
  propC: {
    type: String,
    required: true
  },
  // Number with a default value
  propD: {
    type: Number,
    default: 100
  },
  // Object with a default value
  propE: {
    type: Object,
    // Object or array defaults must be returned from
    // a factory function. The function receives the raw
    // props received by the component as the argument.
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // Custom validator function
  propF: {
    validator(value) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // Function with a default value
  propG: {
    type: Function,
    // Unlike object or array default, this is not a factory function - this is a function to serve as a default value
    default() {
      return 'Default function'
    }
  }
})


//<script setup>を使わないやり方↓
export default {
  props: {
    // 基本的な型チェック
    // (`null` 値と `undefined` 値は、任意の型を許可します)
    propA: Number,
    // 複数の型の可能性
    propB: [String, Number],
    // 必須の文字列
    propC: {
      type: String,
      required: true
    },
    // デフォルト値を持つ数値
    propD: {
      type: Number,
      default: 100
    },
    // デフォルト値を持つオブジェクト
    propE: {
      type: Object,
      // オブジェクトと配列のデフォルトは、ファクトリー関数を使って
      // 返す必要があります。ファクトリー関数は、コンポーネントが
      // 受け取った生の各プロパティを引数として受け取ります。
      default(rawProps) {
        return { message: 'hello' }
      }
    },
    // カスタムのバリデーター関数
    propF: {
      validator(value) {
        // 値が以下の文字列のいずれかに一致する必要がある
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // デフォルト値を持つ関数
    propG: {
      type: Function,
      // オブジェクトや配列のデフォルトと異なり、これはファクトリー関数ではなく、デフォルト値として機能する関数です
      default() {
        return 'Default function'
      }
    }
  }
}

その他の詳細:

  • required: true が指定されていないすべてのプロパティは、デフォルトでオプションです。
  • Boolean 以外のオプションのプロパティは、値が指定されないと undefined 値になります。
  • Boolean のプロパティは、値が指定されないと false に変換されます。望みの動作を得るためには、default の値を指定する必要があります。
  • default の値を指定すると、プロパティの値が undefined に解決される時、それが使用されます。プロパティが指定されなかった場合と、明示的に undefined 値が渡された場合も、これに含まれます。

プロパティのバリデーションに失敗すると、Vue がコンソールに警告を出します (開発ビルドを使用する場合)。

注意

プロパティのバリデーションは、コンポーネントのインスタンスが生成されるに実行されます。そのため、default や validator 関数の中ではインスタンスのプロパティ (例えば datacomputed など) が使用できないことに注意してください。

実行時の型チェック

type には、以下のネイティブコンストラクターを指定することができます:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

加えて、type にはカスタムのクラスやコンストラクター関数を指定することもできます。その場合、instanceof チェックによってアサーションが行われます。例えば、次のクラスがあったとします:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

これをプロパティの型として用いるとします:

defineProps({
  author: Person
})

//<script setup>を使わないやり方↓
export default {
  props: {
    author: Person
  }
}

こうすると、author というプロパティの値が new Person で作成されたものであることをバリデーションで確かめることができます。

ブール値の型変換

Boolean 型のプロパティは、ネイティブのブール値の属性が振る舞う様子を模倣するために、特殊な型変換の規則を持っています。次のような宣言を含む <MyComponent> があるとします:

export default {
  props: {
    disabled: Boolean
  }
}

このコンポーネントは、次のように使用することができます:

<!-- :disabled="true" を渡すのと同等 -->
<MyComponent disabled />

<!-- :disabled="false" を渡すのと同等 -->
<MyComponent />

また、以下のように複数の型を受け付けるようにプロパティを宣言した場合:

export default {
  props: {
    disabled: [Boolean, Number]
  }
}

Boolean の型変換の規則は、型が登場する順序に関係なく適用されます。

[Vue入門]コンポーネントの登録

このページは、すでにコンポーネントの基礎を読んでいることを前提にしています。初めてコンポーネント学ぶ方は、まずそちらをお読みください。

Vue のコンポーネントをテンプレートで使用する時は、それがどこで実装されているかを Vue に知らせるため、「登録」を行う必要があります。コンポーネントの登録方法には、グローバルとローカルの 2 つがあります。

グローバル登録

開発中の Vue アプリケーションでグローバルにコンポーネントを利用できるようにするには、以下に示す app.component() メソッドを使用します:

import { createApp } from 'vue'

const app = createApp({})

app.component(
  // the registered name
  'MyComponent',
  // the implementation
  {
    /* ... */
  }
)

SFC を使用する場合は、インポートした .vue ファイルを登録します:

import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

app.component() メソッドはチェーンにすることができます:

app
  .component('ComponentA', ComponentA)
  .component('ComponentB', ComponentB)
  .component('ComponentC', ComponentC)

グローバル登録したコンポーネントは、アプリケーション内の任意のコンポーネントのテンプレートで使用することができます:

<!-- これはアプリ内のどのコンポーネントでも動作します -->
<ComponentA/>
<ComponentB/>
<ComponentC/>

これは、サブコンポーネントにも漏れなく適用されます。そのため、上の 3 つのコンポーネントはすべて 各コンポーネント内でも互いに 使える、ということになります。

ローカル登録

グローバル登録は便利な反面、以下に示すいくつかの欠点があります:

  1. グローバル登録では、未使用のコンポーネントを削除してくれるビルドシステムの処理 (いわゆる「ツリーシェイク」) が阻害されます。グローバル登録したコンポーネントは、最後までアプリのどこにも用いなかった場合でも、最終的なバンドルには含まれてしまいます。
  2. グローバル登録では、大規模なアプリケーションでの依存関係の分かりやすさが低下します。グローバル登録では、子コンポーネントを使っている親コンポーネントから、子コンポーネントの実装部分を探し出すことが難しくなります。きわめて多くのグローバル変数が使われている状況と同じように、これは長期的な保守性に影響を与える可能性があります。

ローカルでの登録を利用すると、登録したコンポーネントを使えるスコープが現在のコンポーネントのみに限定されます。これによって依存関係が分かりやすくなり、ツリーシェイクが働きやすくなります。

ローカル登録は、以下のように components オプションを使って行います:

でSFCを使用する場合<script setup>、インポートされたコンポーネントは登録なしでローカルで使用できます。

<script setup>
import ComponentA from './ComponentA.vue'
</script>

<template>
  <ComponentA />
</template>

<script setup>を使わない場合は下記のように登録する必要があります。

import ComponentA from './ComponentA.js'

export default {
  components: {
    ComponentA
  },
  setup() {
    // ...
  }
}

オブジェクト内のプロパティごとcomponentsに、キーはコンポーネントの登録名になり、値にはコンポーネントの実装が含まれます。上記の例は、ES2015プロパティの省略形を使用しており、次と同等です。

export default {
  components: {
    ComponentA: ComponentA
  }
  // ...
}

ただし、 ローカル登録されたコンポーネントが子孫のコンポーネントでも利用できるようにはならないことに注意してください。上の場合、ComponentA は現在のコンポーネントのみで利用可能になり、その子や子孫のコンポーネントで利用可能になるわけではありません。

<script setup>を使用してコンポーネントをインポートした場合は、登録が必要ありません。インポートした時点でテンプレートでの使用が可能になります。

コンポーネント名での大文字・小文字の使い方

このガイドでは、コンポーネントを登録する際に PascalCase の名前を用いています。これは次の理由によります:

  1. PascalCase の名前は JavaScript の識別子として有効です。そのため、JavaScript でコンポーネントをインポートしたり登録したりするのが容易になります。また、IDE のオートコンプリートも働きやすくなります。
  2. テンプレートで <PascalCase /> を用いると、これがネイティブの HTML 要素ではなく、Vue のコンポーネントであることがより明確になります。また、Vue コンポーネントとカスタムの要素 (Web Components) を区別することも可能になります。

このスタイルは、SFC や文字列テンプレートを合わせて使う時に推奨されるスタイルです。ただし、DOM テンプレート解析の注意点 で説明しているように、DOM テンプレート内では PascalCase のタグが使えません。

幸いなことに、Vue は PascalCase で登録したコンポーネントから kebab-case 形式のタグへの解決をサポートしています。これにより、MyComponent として登録したコンポーネントは、<MyComponent> と <my-component> のどちらを使ってもテンプレート内で参照できます。そのため、テンプレートの出どころに関わらず、JavaScript のコンポーネント登録のコードには同じものを用いることができます。

[Vue入門] Componentsの基礎

コンポーネントの基礎

コンポーネントによって UI を独立した再利用可能なピースに分割し、それぞれのピースを切り離して考えることができるようになります。アプリケーションはネストされたコンポーネントのツリーによって構成されているのが一般的です:

これは、ネイティブの HTML 要素をネストする方法ととてもよく似ていますが、Vue は独自のコンポーネントモデルを実装しており、各コンポーネントのカスタムコンテンツとロジックをカプセル化することができます。 Vue はまた、ネイティブの Web コンポーネントとうまく連携しています。

コンポーネントの定義

ビルドステップを使用する場合は通常、各 Vue コンポーネントは専用のファイルで .vue 拡張子を使用して定義します。これは 単一ファイルコンポーネント(略して SFC)として知られています:

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

ビルドステップを使用しない場合、Vue コンポーネントは Vue 固有のオプションを含むプレーンな JavaScript オブジェクトとして定義することができます:

export default {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
}

テンプレートは、ここで JavaScript の文字列としてインライン化され、Vue がその場でコンパイルします。また、ID セレクターを使って要素を指定(通常はネイティブの <template> 要素)することもできます。 Vue はそのコンテンツをテンプレート・ソースとして使用します。

上記の例では 1 つのコンポーネントを定義し、それを .js ファイルのデフォルトエクスポートとしてエクスポートしていますが、名前付きエクスポートを使用すると、同じファイルから複数のコンポーネントをエクスポートすることができます。

コンポーネントの使用

TIP

このガイドの残りの部分では SFC 構文を使用します。コンポーネントに関するコンセプトは、ビルドステップを使用するかどうかに関係なく、同じものです。サンプルセクションでは、両方のシナリオでのコンポーネントの使い方をお見せしています。

子コンポーネントを使用するには、親コンポーネントでインポートする必要があります。カウントするコンポーネントを ButtonCounter.vue というファイル内に配置したとすると、このコンポーネントはそのファイルのデフォルトエクスポートとして公開されます:

<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

インポートしたコンポーネントをテンプレートに公開するには、components オプションでコンポーネントを登録する必要があります。これにより、そのコンポーネントは登録されたキーを使ってタグとして利用できるようになります。

また、コンポーネントをグローバル登録することで、インポートすることなくアプリケーション内のすべてのコンポーネントで利用できるようにすることもできます。グローバル登録とローカル登録のメリットとデメリットは、専用のコンポーネントの登録セクションで説明されています。

コンポーネントは好きなだけ、何度でも再利用可能です:

<h1>Here are many child components!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

プレイグラウンドで試す

ボタンをクリックすると、それぞれが別の count を維持することに注意してください。これは、コンポーネントを使用するたびに、新しいインスタンスが作成されるからです。

SFC では、ネイティブの HTML 要素と区別するために、子コンポーネントに パスカルケース のタグ名を使用することが推奨されます。ネイティブの HTML のタグ名は大文字小文字を区別しませんが、Vue の SFC はコンパイルされたフォーマットなので、大文字小文字を区別するタグ名を使うことができます。また、タグを閉じるために /> を使用することができます。

テンプレートを DOM で直接作成する場合(例えば、ネイティブの <template> 要素のコンテンツとして)、テンプレートはブラウザのネイティブな HTML パース動作に従います。そのような場合には、ケバブケース を使用してコンポーネントにクロージングタグを明示する必要があります:

<!-- DOM の中にテンプレートが書かれた場合 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

Props の受け渡し

ブログを構築する場合、ブログの記事を表示するコンポーネントが必要になるかと思います。すべてのブログ記事が同じレイアウトで表示されるようにしたいのですが、コンテンツは異なっています。このようなコンポーネントは、表示したい特定の記事のタイトルや内容などのデータを渡すことができない限り役に立ちません。そこで props の出番です。

props はコンポーネントに登録できるカスタム属性のことです。ブログ記事コンポーネントにタイトルを渡すには、このコンポーネントが受け取る props のリスト内で props オプション を使って props プロパティの使用を宣言する必要があります:

<!-- BlogPost.vue -->
<script>
export default {
  props: ['title']
}
</script>

<template>
  <h4>{{ title }}</h4>
</template>

props 属性に値が渡されると、その値はコンポーネントインスタンスのプロパティになります。プロパティの値は、他のコンポーネントプロパティと同様に、テンプレートの中やコンポーネントの this コンテキストでアクセスすることができます。

コンポーネントは好きなだけ props を持つことができ、デフォルトでどんな値でも、どの props にも渡すことができます。

props が登録されると、以下のようにカスタム属性としてデータを渡すことができるようになります:

<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

しかしながら、一般的なアプリケーションでは親コンポーネントに投稿の配列があることが多いでしょう:

export default {
  // ...
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}

このように各コンポーネントをレンダリングしたい場合は、v-for を使用します:

<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />

プレイグラウンドで試す

v-bind を使って、動的な props を渡すことができることに注目してください。これは、レンダリングするコンテンツを事前に正確に把握していない場合に特に役立ちます。

props については以上となりますが、このページを読み終え内容に慣れてきたら、後ほど Props の完全ガイドを読みにくることをおすすめします。

イベントのリッスン

<BlogPost> コンポーネントを開発していく中で、いくつかの機能については、親コンポーネントへの通信が必要になるかもしれません。例えば、ブログ記事のテキストを拡大し、ページの残りの部分はデフォルトのサイズのままにしておくアクセシビリティ機能を含めることにするかもしれません。

親コンポーネントの中では、postFontSize という data property を追加することで、この機能をサポートできます:

data() {
  return {
    posts: [
      /* ... */
    ],
    postFontSize: 1
  }
}

これは、テンプレート内で使用することができ、すべてのブログ記事のフォントサイズを制御することができます:

<div :style="{ fontSize: postFontSize + 'em' }">
  <BlogPost
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
   />
</div>

では、<BlogPost> コンポーネントのテンプレートにボタンを追加してみましょう:

<!-- BlogPost.vue, omitting <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>Enlarge text</button>
  </div>
</template>

ボタンは今のところ何もしませんが、クリックするとすべての投稿のテキストを拡大表示するように親に伝達したいです。この問題を解決するために、コンポーネントインスタンスはカスタムイベントシステムを提供します。親は子コンポーネントインスタンス上の任意のイベントを、ちょうどネィティブの DOM イベントのように v-on または @ で、リッスンするよう選択できます:

<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

そして、子コンポーネントは組み込みの $emit メソッドを呼び出し、イベント名を渡すことによって自身のイベントを発行することができます:

<!-- BlogPost.vue, omitting <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

enlarge-text="postFontSize += 0.1" リスナーのおかげで、親はイベントを受け取り postFontSize の値を更新することができます。

プレイグラウンドで試す

オプションとして emits オプションを使って emit イベントを宣言することができます:

<!-- BlogPost.vue -->
<script>
export default {
  props: ['title'],
  emits: ['enlarge-text']
}
</script>

コンポーネントが発行する全てのイベントをドキュメント化することで、必要に応じてそれらをバリデーションしています。また、これは Vue が暗黙的に子コンポーネントのルート要素にイベントをネイティブリスナーとして適用するのを避けることにもなります。

カスタムコンポーネントについては以上となりますが、このページを読み終え内容に慣れてきたら、後ほどカスタムイベントの完全ガイドを読みにくることをおすすめします。

スロットを使ったコンテンツ配信

HTML 要素と同じように、以下のようにコンポーネントにコンテンツを渡すことができると便利なことがよくあります:

<AlertBox>
  Something bad happened.
</AlertBox>

これは以下のようなレンダリングがされるかもしれません:

これはデモ目的のエラーです

何らかのエラーが発生しました。

これは Vue のカスタム要素 <slot> を用いて実現することができます:

<template>
  <div class="alert-box">
    <strong>This is an Error for Demo Purposes</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>

上で見たように、コンテンツを配置するプレースホルダーとして <slot> を使う – それだけです。これで完了です!

プレイグラウンドで試す

スロットについては以上となりますが、このページを読み終え内容に慣れてきたら、後ほどスロットの完全ガイドを読みにくることをおすすめします。

動的コンポーネント

ときどきタブ付きインターフェイスのような、コンポーネントを動的な切り替えが役立つ時があります:

プレイグラウンドのサンプルを開く

上記は Vue の <component> 要素の特別な属性 is で実現されています:

<!-- currentTab 変更時にコンポーネントが変わります -->
<component :is="currentTab"></component>

上の例では、:is に渡される値に以下のいずれかを含めることができます:

  • 登録されたコンポーネントの文字列、もしくは
  • 実際にインポートされたコンポーネントオブジェクト

また、is 属性を使って、通常の HTML 要素を作成することもできます。

複数のコンポーネントを <component :is="..."> で切り替えた場合、切り変えられたコンポーネントがアンマウントされます。組み込みの <KeepAlive> コンポーネント を使用すれば、アクティブでないコンポーネントを強制的に “生きて” いる状態にすることができます。

DOM テンプレート解析の注意点

Vue のテンプレートを DOM に直接記述する場合、Vue は DOM からテンプレート文字列を取得する必要があります。これはブラウザのネイティブな HTML パースのふるまいに、いくつかの注意点をもたらします。

TIP

以下で説明する制限事項は、DOM に直接テンプレートを記述する場合にのみ適用されます。以下のソースからの文字列テンプレートを使用する場合は適用されません:

  • 単一ファイルコンポーネント
  • インラインのテンプレート文字列(例: template: '...'
  • <script type="text/x-template">

大文字小文字の区別

HTML タグや属性名は大文字と小文字を区別しないので、ブラウザーはどの大文字も小文字として解釈します。つまり、DOM 内テンプレートを使用する場合、パスカルケースのコンポーネント名、キャメルケースの props 名、v-on イベント名は、すべてケバブケース(ハイフン区切り)を使用する必要があるということになります:

// JavaScript 内ではキャメルケース
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
<!-- HTML 内ではケバブケース -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

自己クロージングタグ

これまでのコードサンプルでは、コンポーネントに自己クロージング (self-closing) タグを使用していました:

<MyComponent />

これは、Vue のテンプレートパーサーが /> を、タグの種類に関係なく任意のタグを終了する指示として尊重するためです。

しかし、DOM テンプレートでは必ず明示的なクロージングタグを入れる必要があります:

<my-component></my-component>

これは HTML の仕様では、いくつかの特定の要素でのみ自己クロージングタグの省略が認められているからです。最も一般的なのは <input> と <img> です。他のすべての要素では、自己クロージングタグを省略すると、ネイティブの HTML パーサーは開始タグを終了させなかったと判断します。例えば、次のようなスニペットです:

<my-component /> <!-- ここがクロージングタグのつもりです -->
<span>hello</span>

このようにパースされます:

<my-component>
  <span>hello</span>
</my-component> <!-- ですが、ブラウザーはここでクローズします -->

要素の配置制限

<ul> 、 <ol> 、 <table> 、 <select> など、一部の HTML 要素には内部に表示できる要素に制限があります。例えば <li> などの一部の要素には、 <tr> 、および <option> は特定の要素内にのみ表示できます。

このような制限のある要素でコンポーネントを使用する場合に問題が発生します。例えば:

<table>
  <blog-post-row></blog-post-row>
</table>

カスタムコンポーネント <blog-post-row> は無効なコンテンツとして巻き上げられ、最終的なレンダリング出力でエラーが発生します。回避策として、特別な is 属性 を使用することができます:

<table>
  <tr is="vue:blog-post-row"></tr>
</table>

TIP

ネイティブの HTML 要素で使用する場合、Vue コンポーネントとして解釈されるためには is の値の前に vue: を付けなければなりません。これはネイティブの組み込みのカスタマイズ要素との混同を避けるために必要となります。

DOM テンプレート解析の注意点については、以上で終わりです。

[Vue入門] Template Refs

テンプレート参照

Vue の宣言型レンダリングモデルは、直接的な DOM 操作のほとんどを抽象化してくれます。それでも、基盤の DOM 要素に直接アクセスすることが必要になるケースがまだ存在するかもしれません。次に示す ref という特殊な属性を用いると、それを実現することができます:

<input ref="input">

ref は、v-for の章で説明した key 属性に似た、特殊な属性です。これを使用すると、特定の DOM 要素や子コンポーネントのインスタンスがマウントされた後に、そのインスタンスへの直接の参照を取得することができます。例えば、コンポーネントがマウントされた時にプログラムを使って入力欄にフォーカスを当てたり、ある要素に使用するサードパーティのライブラリーを初期化したりしたい時に便利です。

参照へのアクセス

結果として得られる参照は、以下のように this.$refs で公開されます:

<script>
export default {
  mounted() {
    this.$refs.input.focus()
  }
}
</script>

<template>
  <input ref="input" />
</template>

参照にアクセスできるのは、コンポーネントがマウントされた後に限られることに注意してください。テンプレートの式で $refs.input にアクセスしようとしても、初回のレンダリングでは null になっています。なぜなら、初回のレンダリングが終わった後でないと要素が存在しないためです!

v-for の中の参照

v3.2.25 以降が必要です。

v-for の中で ref を使用すると、結果として得られる参照の値は、対応する要素を格納する配列になります:

<script>
export default {
  data() {
    return {
      list: [
        /* ... */
      ]
    }
  },
  mounted() {
    console.log(this.$refs.items)
  }
}
</script>

<template>
  <ul>
    <li v-for="item in list" ref="items">
      {{ item }}
    </li>
  </ul>
</template>

プレイグラウンドで試す

参照の配列では、元の配列と同じ順序が保証されないことに注意する必要があります。

関数を使った参照

ref 属性は、文字列のキーの代わりに、関数にバインドすることもできます。関数はコンポーネントが更新されるたびに呼び出され、要素の参照をどこに保持するかを柔軟に決めることができます。関数は、第 1 引数として要素への参照を受け取ります:

<input :ref="(el) => { /* el をプロパティまたは ref に保持する */ }">

動的な :ref のバインディングを使っていることに注目してください。これにより、参照の名前を示す文字列ではなく、関数を渡すことが可能になります。要素がアンマウントされると、引数は null になります。もちろん、インライン関数のほかに、メソッドを指定することもできます。

コンポーネントでの参照

このセクションでは、コンポーネントの知識があることが前提となります。読み飛ばして、後で戻ってきても大丈夫です。

ref は子コンポーネントに対して使用することもできます。その場合、以下のように、参照はコンポーネントのインスタンスへの参照になります:

<script>
import Child from './Child.vue'

export default {
  components: {
    Child
  },
  mounted() {
    // this.$refs.child は <Child /> のインスタンスを保持します。
  }
}
</script>

<template>
  <Child ref="child" />
</template>

参照されるインスタンスは子コンポーネントの this と同じになります。これは、親コンポーネントからは子コンポーネントのすべてのプロパティとメソッドに完全にアクセスできることを意味します。そうなると、親と子の間で実装の細かな部分が緊密に結合された状態が作られやすくなってしまいます。したがって、コンポーネントの参照は、絶対に必要と言える場合に限って使用するべきです。ほとんどの場合、まずは標準の props と emit のインターフェースを使って親子間のやり取りを実装することを試みるとよいでしょう。

子インスタンスへのアクセスに制限を設けるには、expose オプションを使用します:

export default {
  expose: ['publicData', 'publicMethod'],
  data() {
    return {
      publicData: 'foo',
      privateData: 'bar'
    }
  },
  methods: {
    publicMethod() {
      /* ... */
    },
    privateMethod() {
      /* ... */
    }
  }
}

上の例では、テンプレート参照を用いてこのコンポーネントを参照する親に、publicData と publicMethod のみへのアクセスを許可します。

[Vue入門] Watchers

Watchers

基本の例

算出プロパティを使うと、派生した値を宣言的に算出することができるようになります。しかしながら、状態の変更に応じて「副作用」を実行する必要とする場合があります。たとえば、DOM が変化する、あるいは非同期処理の結果に基づいて、別の状態にに変更した場合といったものです。

Option API では、watch オプション を使って、リアクティブなプロパティが変更されるたびに関数を実行することができます:

<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')

// watch works directly on a ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.indexOf('?') > -1) {
    answer.value = 'Thinking...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API. ' + error
    }
  }
})
</script>

<template>
  <p>
    Ask a yes/no question:
    <input v-model="question" />
  </p>
  <p>{{ answer }}</p>
</template>

//下は<script setup>でないcompostionAPIのやり方

export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)'
    }
  },
  watch: {
    // 問題内容が変更されるたびに、関数が実行されます。
    question(newQuestion, oldQuestion) {
      if (newQuestion.indexOf('?') > -1) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.answer = 'Thinking...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Error! Could not reach the API. ' + error
      }
    }
  }
}
<p>
  Ask a yes/no question:
  <input v-model="question" />
</p>
<p>{{ answer }}</p>

プレイグラウンドで試す

watch オプションはドットで区切られたパスをキーとして使うこともできます。

export default {
  watch: {
    // 注意 単純なパスのみ対応しています。式は対応していません。
    'some.nested.key'(newValue) {
      // ...
    }
  }
}

Deep Watchers

watch はデフォルトではネストが浅い場合にしか対応していません: そのため、コールバックは監視対象のプロパティに新しい値が割り当てられた場合にしか実行されません。- そのため、ネストしたプロパティの変更があった場合には実行されません。もし、ネストしたすべての変更でコールバックが実行されるようにする場合、deep watcher を使用する必要があります。

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // fires on nested property mutations
  // Note: `newValue` will be equal to `oldValue` here
  // because they both point to the same object!
})

obj.count++


//下は<script setup>でないcompostionAPIのやり方


export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // 注意:オブジェクト自体が置き替わらない限り、
        // ネストした変更では、 `newValue` は、`oldValue` と
        // 等しくなります。
      },
      deep: true
    }
  }
}

使用上の注意

deep watch は、監視対象のオブジェクトのネストされた全てのプロパティをトラバースする必要があるため、大きなデータ構造で使用するときにはコストが高くなります。使用するときは、どうしても必要なときにだけ使用し、パフォーマンスへの影響に注意しましょう。

Eager Watchers

watch は、デフォルトでは、遅延して実行されます: 監視対象の値が変更するまでコールバックは実行されません。しかし、同様のコールバックのロジックを先に実行したい場合もあります。- たとえば、初期値のデータを読み込み、関連する状態が変更されるたび、再びデータを読み込みたいときです。

handler 関数と immediate: true オプションを設定したオブジェクトを利用して宣言することで、監視対象のコールバック関数をすぐ実行させることができます:

export default {
  // ...
  watch: {
    question: {
      handler(newQuestion) {
        // コンポーネントが生成されるとすぐに実行されます。
      },
      // 前倒しして、コールバックの実行を強制します。
      immediate: true
    }
  }
  // ...
}

コールバックが実行されるタイミング

リアクティブな状態が変更されるとき、Vue コンポーネントの更新と生成された watcher コールバックを実行します。

デフォルトでは、ユーザーが生成した watcher のコールバックは Vue コンポーネントが更新される前に呼ばれます。これはつまり、コールバック内で DOM へアクセスしようとすると、DOM は Vue が更新を適用される前の状態です。

もし Vue の更新に watcher コールバック内で DOM へアクセスしたいとき、flush: 'post' オプションで指定する必要があります:

export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'post'
    }
  }
}

this.$watch()

また、$watch() インスタンスメソッド を使用して watcher を強制的に作成することが可能です:

export default {
  created() {
    this.$watch('question', (newQuestion) => {
      // ...
    })
  }
}

これは、条件付きで watcher をセットアップする必要があるときや、ユーザーの相互作用に応じる場合にのみ、何かを監視しないといけないときに役立ちます。これにより、watcher を早い段階で停止することができます。

Watcher の停止

watch オプションを使って宣言した watchers、あるいは $watch() インスタンスメソッドは、オーナーコンポーネントがアンマウントされた自動的に停止します。そのため、多くの場合において、watcher 自体が停止することを心配する必要はありません。

ごくまれに、オーナーコンポーネントがアンマウントされる前に停止する必要がある場合には、$watch() API は次のような関数を返します:

const unwatch = this.$watch('foo', callback)

// ...wathcer が必要なくなったとき:
unwatch()

watchEffect()

watchEffectはlazyで、監視する対象のソースが変わるまでコールバックが起きません。

const url = ref('https://...')
const data = ref(null)

async function fetchData() {
  const response = await fetch(url.value)
  data.value = await response.json()
}

// fetch immediately
fetchData()
// ...then watch for url change
watch(url, fetchData)

動画で紹介したコード

<template>

  <p>敵のHP:{{enemyHP}}</p>
  <p>ヒーローのHP:{{heroHP}}</p>
<button :class="msgClass" @click.prevent="attack">攻撃</button>
<p >{{msg}}</p>
</template>

<script setup>
import { watch, ref, watchEffect } from 'vue';

const enemyHP = ref(100)
const heroHP = ref(40)
const msg = ref("")
const msgClass = ref("")


const attack = () => {
  enemyHP.value = enemyHP.value - Math.floor(Math.random() * 10)
  heroHP.value = heroHP.value - Math.floor(Math.random() * 5)
  msg.value = ""
}

watch(heroHP, (currHP, prevHP) => {
  console.log(currHP, prevHP)
  if (currHP < 20) {
    msg.value = "HPが半分をきった。まずい。"
  }
  if (prevHP - currHP > 3) {
    msg.value = "会心の一撃を喰らった。"
  }
})

watchEffect(async () =>
  heroHP.value == 40 ? msgClass.value = "blue" :
  heroHP.value <= 20 ? msgClass.value = "red" : ""
)
</script>

<style>
.blue {
  color:blue
}

.red {
  color:red
}
</style>