Open8

気ままに Vue.js3 復習

オクトオクト

reactive()

https://vuejs.org/guide/essentials/reactivity-fundamentals.html#declaring-reactive-state

まずは読み込みが必要。

import { reactive } from 'vue'

🐙 reactive()ってなんだっけ?

reactive()はJavaScript Proxyであり、普通のオブジェクトのように振る舞うもの。雑にいうと、オブジェクト。
TODO: Proxyを学習せな

🐙 普通のオブジェクトと何が違うねん!

reactive()は、一貫した参照を保証してくれる。
どういうことかというと、同じオブジェクトに対してreactive()を呼ぶと、同じプロキシを返す。
同じプロキシに対して再度reactive()を呼ぶと、同じプロキシが返ってくる。

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

  const raw = {}
  const proxy = reactive(raw)

  console.log(reactive(raw) === proxy)  //=> true
  console.log(reactive(proxy) === proxy)  //=> true
</script>

🐙 でも、reactive()にも欠点があるんでしょ?

  1. ObjectArraycollection typeつまりMap Object Set Objectのみ動く。もちろん、その反対のString, Numberなどのプリミティブタイプには動かない。
  2. 再代入したり、他の変数に格納すると、追跡が途切れる。
sample code
<script setup>
  import { reactive } from 'vue'

  const state = reactive({ count: 0 })

  let n = state.count
  n++

  console.log(n === state.count)  //=> false
  console.log(`${n} === ${state.count} ??`)  //=> 1 === 0 ??

  let { count } = state
  console.log(count)  //=> 0

  count++

  console.log(count === state.count)  //=> false
  console.log(`${count} === ${state.count} ??`)  //=> 1 === 0 ??
</script>

TODO: Map Object Set Objectを深く学習せな

オクトオクト

Component Event

🐙 そういえばデータの流れは基本一方向で親から子へと流れるんだっけ?

基本的にはそう。
https://vuejs.org/guide/components/props.html#one-way-data-flow

🐙 じゃあ、子から親に送りたいときはどうするんだ?

そういうときはemitを使うことで、子のコンポーネントはイベントを送って親にデータの更新を行わせる。

🐙 これなんかややこしいんだよね

子コンポーネントでは$emitを使って、イベント名を送っている。
親コンポーネントでは@eventName:eventFuncを使って、イベント名と実行したい関数を定義する。よくリッスンするとか言ってるやつ。

Child.vue
<button @click="$emit('someEvent')">Click me</button>
Parent.vue
<Child @some-event="callback" />

気付いたかもしれないが、templateの中ではkebab-casedを使うことをおすすめされている。
詳しくはこちらで確認しよう。

Event Arguments

🐙 もし子コンポーネントから引数に値を渡したいときとかはどうするの?

$emit('<eventName>', args...)のように第2引数以降で渡すことができる。

sample code
Child.vue
<script setup>
  defineEmits(['increment'])
</script>

<template>
  <button @click="$emit('increment', 1)">Click me</button>
</template>

Parent.vue
<script setup>
  import Child from './components/Component.vue'
  import { ref } from 'vue'

  const count = ref(0)
  const incrementNumber = (n) => {
    count.value += n
  }
</script>

<template>
  <Child @increment="incrementNumber" />
  <p>{{ count }}</p>
</template>

$emitには第2引数以降、複数の値を渡すことができる。
$emit('hoge', 1, 2, 3...)みたいな感じ。

🐙 defineEmits()はなんなんや?

<template>に記述した$emit<script setup>の中では参照できない。
defineEmits()は同じ関数を返してくれるので参照できるようになる。
だからこんな感じで使えるようになるみたい。いまいちピンときてない。。。

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

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

defineEmits()<script setup>内のトップレベルでのみ使える。つまり、関数などの中では使えない。

setup()を使っている場合は、こんな感じで使えるみたい。

export default {
  emits: ['inFocus', 'submit'],
  setup(props, { emit }) {  // (props, ctx)
    emit('submit')  // ctx.emit('submit')とすることもできる
  }
}

すべてのイベントに対して、defineEmits()を使って定義することを公式では薦められている。なんか色んな問題を回避できるっぽい。

Events Validation

🐙 カスタムイベントに対して、バリデーションをつけたいな〜

できます。
defineEmits()に対して、Arrayを渡す代わりにObjectを渡す。
<script setup>を使わないなら、emits:オプションにObjectを渡す。
keyにあたる部分がカスタムイベントの名前を、valueにあたる部分がバリデーションの関数を定義する。

sample code
Child.vue
<script setup>
  import { ref } from 'vue'

  const name = ref('')
  const close = defineEmits({
    close: (name) => {
      if (!name) {
        return false
      }

      return true
    }
  })
</script>

<template>
  <div>
    <h2>This is a Popup</h2>
    <input
      type="text"
      v-model="name"
    />
    <button @click="$emit('close', name)">Close Popup</button>
  </div>
</template>
Parent.vue
<script setup>
  import Child from './components/Component.vue'
  import { ref } from 'vue'

  const showPopup = ref(false)
  const closePopup = (name) => {
    showPopup.value = false
    console.log(`Entered name is ${name}`)
  }
</script>

<template>
  <button @click="showPopup = true">Show Popup</button>
  <Child v-show="showPopup" @close="closePopup" />
</template>

Custom Events with v-model

🐙 カスタムイベントに対してv-modelを使いたいんだけど、どうすれば良いのだ?

そのためには2つの条件を満たす必要がある。

  1. ネイティブのinput要素のvalue属性とmodelValueをバインディングする。
  2. ネイティブのinput要素がトリガーされた時、新しい値と一緒にupdate:modelValueを親のコンポーネントに送る。

親のコンポーネントでは、v-modelで紐づければ良い。

sample code
Child.vue
<script setup>
  import { ref } from 'vue'

  defineProps(['modelValue'])
</script>

<template>
  <div>
    <h2>This is a input</h2>
    <input
      type="text"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    />
  </div>
</template>
Parent.vue
<script setup>
  import Child from './components/Component.vue'
  import { ref } from 'vue'

  const message = ref('hello')
</script>

<template>
  <Child v-model="message" />
  <p>{{ message }}</p>
</template>

もう一つ方法がある。
それは、getterとsetterを使って書き込み専用のcomputedプロパティを使うことだ。
getメソッドは、modelValueを返して、setメソッドは一致するイベントを送る。

sample code
Child.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>
  <div>
    <h2>This is a input</h2>
    <input type="text" v-model="value">
  </div>
</template>

🐙 ねえ、これってmodelValueとか決められたものしか使えないの?

結論、使える。
使うためには子コンポーネントで、v-model:<valueName>$emit('update:<valueName>', ~~~)を定義して親コンポーネントの値を更新してやれば良い。
この時、親コンポーネントでは、v-model:<valueName>="<val>"として結びつける。

sample code
Child.vue
<script setup>
  defineProps(['name'])
  defineEmits(['update:name'])
</script>

<template>
  <div>
    <h2>This is a input</h2>
    <input
      type="text"
      :value="name"
      @input="$emit('update:name', $event.target.value)"
    />
  </div>
</template>
Parent.vue
<script setup>
  import Child from './components/Component.vue'
  import { ref } from 'vue'

  const name = ref('hello')
</script>

<template>
  <Child v-model:name="name" />
  <p>{{ name }}</p>
</template>

🐙 でも、複数のv-modelが必要になるときの方が多くない?どうするの?

一つ上でしたことを応用すれば良い。
親のコンポーネントでv-model:<valueName>="<val>"とすることで、複数あっても仕分けることができるようにする。

sample code
Child.vue
<script setup>
  defineProps({
    firstName: String,
    lastName: String
  })
  defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <div>
    <h2>Enter Your Name!</h2>
    <input
      type="text"
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)"
    />
    <input
      type="text"
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)"
    />
  </div>
</template>
Parent.vue
<script setup>
  import Child from './components/Component.vue'
  import { ref } from 'vue'

  const firstNameValue = ref('Riho')
  const lastNameValue = ref('Yoshioka')
</script>

<template>
  <Child
   v-model:firstName="firstNameValue"
   v-model:lastName="lastNameValue"
  />
  <p>{{ lastNameValue + ' ' + firstNameValue }}</p>
</template>

Handling v-model modifiers

🐙 v-modelのバインディングを学んだときに、.trimとかの修飾子も学んだけど、これもカスタムできるの?

できる!
modelModifiersを通してコンポーネントにカスタム修飾子を提供できる。

親コンポーネントでv-model.<custom modifiers>とした<custom modifiers>の箇所がカスタム修飾子の名前になる。
そして、子コンポーネントのmodelModifiersの中にカスタム修飾子が含まれる。defineProps()の中に含めないといけないのを忘れないように。

sample code
Chil.vue
<script setup>
  const props = defineProps({
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  })

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

  const 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>
  <div>
    <input type="text" :value="modelValue" @input="emitValue">
  </div>
</template>
Parent.vue
<script setup>
  import Child from './components/Component.vue'
  import { ref } from 'vue'

  const myText = ref('')
</script>

<template>
  <h2>This input capitalizes everything you enter:</h2>
  <Child v-model.capitalize="myText" />
</template>

カスタムイベントに対して、カスタム修飾子がつく場合、登録されるカスタム修飾子の名前は<custom eventName> + <Modifiers>となる。

<MyComponent v-model:title.capitalize="myText">
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])

console.log(props.titleModifiers) //=> { capitalize: true }
オクトオクト

Fallthrough Attributes

🐙 Fallthrough Attributesって聞いたんだけど、これは一体なんだい?

Fallthrough Attributesは、あるコンポーネントに渡したんだけど、渡されたコンポーネントでpropsemitsで明示的に宣言されていない属性もしくはv-onイベントリスナーのこと。

Fallthroughは、抜け落ちる、〈計画などが〉失敗に終わる、むだになるっていう意味。

Fallthrough Attributesは、一つのコンポーネントをレンダリングするときに、自動的にルート要素の属性に追加される。

🐙 どういうこと?

こういうことだ。

Child.vue
<template>
  <button>Click me</button>
</template>
Parent.vue
<script setup>
  import Child from './components/Component.vue'
</script>

<template>
  <Child class="large" />
</template>

親コンポーネントで<Child class="large" />のように使った場合、レンダリングされると<button class="large">Click me</button>となる。
<button>Click me</button><div>でラップすると、今度は<div class="large">になる。
兄弟要素に<p>タグなどを入れると、class="large"はどこにも割り当てられない。

classstyle属性のマージ

🐙 既に子コンポーネントでclassstyle属性が定義されていた場合、どうなるの?

この場合、親コンポーネントで渡した値が子コンポーネントの属性にマージされる。

<button class="btn">Click me</button>があってレンダリングされると、<button class="btn large">Click me</button>となる。

🐙 これってclass属性やstyle属性だけなの?

いや、v-onディレクティブも同じ効果を持つ。
親コンポーネントと子コンポーネントどちらもv-onディレクティブでバインドされたclickイベントリスナーを持つ場合、どちらのイベントも発火する。

Child.vue
<script setup>
  const componentEvent = () => {
    console.log('componentEvent is fired!')
  }
</script>

<template>
  <button @click="componentEvent">Click me</button>
</template>
Parent.vue
<script setup>
  import Child from './components/Component.vue'
  
  const parentEvent = () => {
    console.log('parentEvent is fired!')
  }
</script>

<template>
  <Child @click="parentEvent" />
</template>

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

🐙 コンポーネントの中にコンポーネントを呼び出す、いわゆる孫コンポーネントの場合はどうなるの?

基本的には、Fallthrough Attributesは自動的に孫コンポーネントへ転送される。

Nested.vue
<script setup>
  const componentEvent = () => {
    console.log('Nested component event is fired!')
  }
</script>

<template>
  <button @click="componentEvent">Click nested me</button>
</template>
Child.vue
<script setup>
  import Nested from './Nested.vue'
</script>

<template>
  <Nested />
</template>
Parent.vue
<script setup>
  import Child from './components/Component.vue'
  
  const parentEvent = () => {
    console.log('parentEvent is fired!')
  }
</script>

<template>
  <Child @click="parentEvent" />
</template>

ここでコンソールに表示されるのは以下になる。

Nested component event is fired!
App.vue:5 parentEvent is fired!

ただし、例外として

継承の無効化

🐙 例えば、ルート要素の子要素にFallthrough Attributesを適用させたいケースはどうなるの?

そういうケースは、inheritAttrs: falseをFallthrough Attributesを受け取る子孫コンポーネントの<script>タグ内で定義する。

ここで注意が必要なのは<script setup>を使っていても、別で<script>タグを定義してその中にオプションを置く必要がある。

このケースのFallthrough Attributesを参照するためには$attrsで参照できる。
この$attrsの中にはpropsemitsで宣言されていない全ての属性が含まれている。

例えば、孫コンポーネントのルート要素を<div>にして、その中に<button>タグを置いて、Fallthrough Attributesをこの<button>タグに渡したいケースを考える。(子コンポーネントは1つ前のセクションと同じ構造)
今回は、classv-onイベントを渡す。inheritAttrs: falsev-bind="$attrs"で実現させる。

Nested.vue
<script>
  export default {
    inheritAttrs: false
  }
</script>

<script setup>
  const componentEvent = () => {
    console.log('Nested component event is fired!')
  }
</script>

<template>
  <div>
    <button v-bind="$attrs" @click="componentEvent">Click nested me</button>
  </div>
</template>
Parent.vue
<script setup>
  import Child from './components/Component.vue'
  
  const parentEvent = () => {
    console.log('parentEvent is fired!')
  }
</script>

<template>
  <Child @click="parentEvent" class="btn" />
</template>

🐙 じゃあ、ルートノードに複数の要素があった場合は、どうなるの?

そのケースも、v-bind="$attrs"として、明示的にバインドする必要があるよ。

🐙 JavaScriptでもそのFallthrough Attributesにアクセスできるの?

できる!
そのときは、<script setup>内でuseAttrs()を使って参照できる。

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

  const attrs = useAttrs()
</script>

<script setup>を使わない場合は、setup()の中のプロパティから参照できる。

setup(props, ctx) {
  console.log(ctx.attrs)
}
オクトオクト

Computed Properties

🐙 算出プロパティって何だっけ?

テンプレートの中で、リアクティブな値を含む複雑なロジックを組んでいくと、可読性や記述量の観点から好ましくない。そのようなケースに適しているのが、算出プロパティである。

<script>内で参照する場合は.valueを使う。<template>内ではアンラップされるため、.valueを使う必要はなく、そのまま参照すればOK!

<script setup>
  import { reactive, computed } from 'vue'

  const baseball = reactive({
    team: [
      'DeNA Baystars',
      'Tokyo Yakult',
      'Hanshin Tygers',
    ]
  })

  const isBaseballTeam = computed(() => {
    return baseball.team.length > 0 ? 'Team is existed.' : 'No team.'
  })

  console.log(isBaseballTeam.value)  //=> Team is existed.
</script>

<template>
  <h1>BaseBall</h1>
  <p>{{ isBaseballTeam }}</p>  <!-- Team is existed. -->
</template>

算出プロパティは、リアクティブな依存関係も追跡してくれる。
元のリアクティブな値(上のケースで言うと、baseball.team)が更新されると、算出プロパティに依存している全てのバイディングも更新される。

🐙 あれ?普通のメソッドと算出プロパティって何が違うの?

結論、キャッシュされるか否か、だよ。
算出プロパティは、一度実行されると依存関係に基づいてキャッシュする。リアクティブな依存関係が更新されたときに、再度実行される。

なので、上記のサンプルのbaseball.teamが更新されない限りは、isBaseballTeamは何度参照されたとしても、以前計算された結果を返すだけ。

通常のメソッドの場合は、呼び出されるたび(レンダリングされるたび)にgetter関数を実行して結果を返す。用途としては、キャッシュしたくないときに通常のメソッドを定義すればOK!

🐙 算出プロパティに新しい値を代入することはできるの?

結論はできる。
ただし、算出プロパティはデフォルトでgetter関数しか提供していないので、setter関数を定義する必要がある。

const something = computed({
  get() {
    // implement something
  },
  set() {
    // implement something
  },
})
オクトオクト

Watchers

🐙 ウォッチャーって何だっけ?

Watchersは、データやcomputedで定義したプロパティの状態の変化を監視して、変更されたときに任意の処理を実行してくれる仕組みのこと。つまり、変更されるたびに実行される。

🐙 どんな感じで使っていくの?

watchの第1引数は、refreactiveやgetter関数や配列なんかも渡すことができる。
ただし、reactiveオブジェクトのプロパティは渡しても監視されないことに注意。
代替手段としては、getter関数を渡せばいける。

例えば、

const bucket = reactive({ count: 0 })

watch(bucket.count, ~~~)  // bucket.countはプロパティなので監視されない

watch(() => bucket.count, ~~~)  //  getter関数を使えばいける

🐙 ディープウォッチャーっていう、なんだかカッコ良い名前が出てきた

これはリアクティブなオブジェクトに対して、watch関数を呼び出すとき、自動的にディープウォッチャーが作られるんだ。全てのネストしたプロパティが変更されたときに実行されるんだよ。

sample code
<script setup>
  import { reactive, watch } from 'vue'

  const person = reactive({
    id: 0,
    name: 'Riho Yoshioka',
    age: 29,
    hobby: {
      first: 'Reading book',
    },
  })

  watch(
    person,
    (newValue, oldValue) => {  // newValueとoldValueは同じオブジェクトであるため、同じものとしてみなされる。
      console.log('watch changing!')
    },
  )
</script>

<template>
  <h1>Enter something text</h1>
  <input type="number" v-model="person.age">  <!-- 変更するとwatch関数が動く -->
  <input type="text" v-model="person.hobby.first">  <!-- 変更するとwatch関数が動く -->
</template>

🐙 でも、例えばageが変更されてもwatch関数が動いて欲しくない時もあるけど、そういう時は?

watch関数で指定してあげれば良い

watch(() => person.name, (newValue, oldValue) => {
  console.log(newValue)  // person.nameが変更されたときだけ実行される
})

deep: trueを第3引数に渡すと、強制的にディープオプションを作ることができる。

sample code
<script setup>
  import { reactive, watch } from 'vue'

  const person = reactive({
    id: 0,
    name: 'Riho Yoshioka',
    age: 29,
    hobby: {
      first: 'Reading book',
    },
  })

  watch(
    () => person.hobby,
    (newValue, oldValue) => {
      console.log('watch changing!')
    },
    { deep: true }  // deepオプションがtrueになっていると、深くネストしたものに対しても監視できる。これを外すとwatch関数は動かない。
  )
</script>

<template>
  <h1>Enter something text</h1>
  <input type="number" v-model="person.age">
  <input type="text" v-model="person.hobby.first">
</template>

watchEffect()

🐙 これはwatchと何が違うんだ?

違いは主にこれだ。
watch: 遅延実行、監視対象を明示的に指定、変更前・変更後の値を取れる

watchEffect: 即時実行、関数内のリアクティブオブジェクト全てが監視対象

この記事がめちゃ分かりやすかった。
https://qiita.com/doz13189/items/d09cfc6e1ff38621c2cc

この記事でも同じようなことを述べている。
https://www.vuemastery.com/blog/vues-watch-vs-watcheffect-which-should-i-use/

code
<script setup>
  import { reactive, watch, watchEffect } from 'vue'

  const person = reactive({
    id: 0,
    name: 'Riho Yoshioka',
    age: 29,
    hobby: {
      first: 'Reading book',
    },
  })

  watch(
    person,
    (newValue, oldValue) => {
      console.log('watch changing!')  // 読み込み時は実行せずに、リアクティブオブジェクトが変更されたときに実行される。
    },
  )

  watchEffect(() => {  // 読み込み時に実行され、リアクティブオブジェクトが変更されたときも実行される。
    console.log('watchEffect is executed.')
    console.log(`person's age is ${person.age}.`)
  })
</script>

<template>
  <h1>Enter your age!</h1>
  <input type="number" v-model="person.age">
</template>

ただし、非同期の処理の場合は、awaitの前に参照されるプロパティのみ追跡する。

🐙 あれ?watchを使っているときにDOMへアクセスしたいのだけれど、更新前の状態だ。。。

リアクティブオブジェクトが更新されて、watchwatchEffectが実行されるタイミングはVueコンポーネントが更新される前に実行される。

Watchersの停止

同期的にwatchwatchEffectを定義する場合、Vueコンポーネントがアンマウントされるときに、自動でwatchwatchEffectを停止してくれる。

しかし、非同期でそれらが生成された場合、自動で停止してくれずメモリリークを引き起こしてしまう可能性があるため、手動で停止させる必要がある。

停止させるためには、watchwatchEffectから返される関数を実行する。

const unwatch = watch(~~~)

unwatch()

出来れば、同期的に生成することが一番望ましい。
しかし、非同期で生成しないといけないケースも出てきた場合は、条件付きを検討してみよう。

watch(source, () => {
  if(data.value) {
    // implement something
  }
})
オクトオクト

slot

レンダースコープ

親のコンポーネントから渡す(定義されている)テンプレートは、親のコンポーネントで定義されているリアクティブオブジェクトを参照できる。
しかし、親で定義されたテンプレートでは、子のコンポーネントで定義されたリアクティブオブジェクトを参照することはできない。

つまり、親コンポーネントで定義された式やpropsは親コンポーネント内のみ参照でき、子コンポーネントで定義された式やpropsは子コンポーネント内のみ参照できる。

success code
Parent.vue
<script setup>
  import Component from './components/Component.vue'
  import { ref } from 'vue'

  const name = ref('Mike')
</script>

<template>
  <Component>{{ name }}</Component>
</template>
Child.vue
<script setup>
  import { ref } from 'vue'

  const clickLabel = ref('click me!')
</script>

<template>
  <button>
    <slot></slot>
  </button>
</template>
bad code
Parent.vue
<script setup>
  import Component from './components/Component.vue'
  import { ref } from 'vue'

  const name = ref('Mike')
</script>

<template>
  <Component>{{ name }}</Component>
</template>
Child.vue
<script setup>
  import { ref } from 'vue'

  const clickLabel = ref('click me!')
</script>

<template>
  <button>
    <slot></slot>
    {{ name }}  <!-- [Vue warn]: Property "name" was accessed during render but is not defined on instance. -->
  </button>
</template>

名前付きスロット

🐙 slotを複数定義したい場合はどうするの?

slotタグのname属性に値を定義することで、一意のIDを付与することができる。
*何も指定しない場合、defaultという名前で登録される。

テンプレートを渡す側のコンポーネントでは、<template>タグにv-slot:<slot name>ディレクティブを定義すると、テンプレートを渡すことができる。
この省略記法は<template #<slot name>で定義できる。

sample code
Child.vue
<script setup>
  import { ref } from 'vue'

  const clickLabel = ref('click me!')
</script>

<template>
  <div class="name">
    <slot></slot>
  </div>
  <div class="address">
    <slot name="address"></slot>
  </div>
  <div class="email">
    <slot name="email"></slot>
  </div>
</template>
Parent.vue
<script setup>
  import Component from './components/Component.vue'
  import { ref } from 'vue'

  const name = ref('Mike')
  const address = ref('Tokyo')
  const email = ref('sample@example.com')
</script>

<template>
  <Component>
    <p>{{ name }}</p>
    <template #address>
      <p>{{ address }}</p>
    </template>
    <template v-slot:email>
      <p>{{ email }}</p>
    </template>
  </Component>
</template>

Dynamic Slot Name

🐙 もっと柔軟にslotの名前が変わるケースって対応できるの?

slotの名前は動的に定義できる。
その方法は、テンプレートをスロットに渡す側のコンポーネントの<template>v-slot:[slotName]とすることで動的に変えられる。

sample code
Child.vue
<script setup>
  import { ref } from 'vue'

  const clickLabel = ref('click me!')
</script>

<template>
  <div class="name">
    <slot name="name">default name</slot>
  </div>
  <div class="address">
    <slot name="address">default address</slot>
  </div>
  <div class="email">
    <slot name="email">default email</slot>
  </div>
</template>
Parent.vue
<script setup>
  import Component from './components/Component.vue'
  import { ref } from 'vue'

  const name = ref('Mike')
  const address = ref('Tokyo')
  const email = ref('sample@example.com')
  const toggle = ref('name')
</script>

<template>
  <button @click="toggle = 'name'">name</button>
  <button @click="toggle = 'address'">address</button>
  <button @click="toggle = 'email'">email</button>
  <Component>
    <template #[toggle]>
      <p>{{ toggle }}</p>
    </template>
  </Component>
</template>

スコープ付きスロット

🐙 でも、親は親のスコープ、子は子のスコープっていうのは、閉じられていて良さそうだけど、両方から来るデータを扱えると便利だよな〜

レンダリングするときに、子コンポーネントがデータをスロットに渡すことができれば実現でき、その機能を提供している。
ルールが、一つのみのデフォルトスロットと名前付きスロットで変わる。

一つのみのデフォルトスロットでは、子コンポーネントで:<value name>="<value>"のように渡してあげれば良い。親コンポーネントでは、v-slot="<Name>"のように受け取れば良い。

sample code
Child.vue
<script setup>
  import { ref } from 'vue'

  const nameValue = ref('Riho Yoshioka')
  const ageValue = ref(29)
</script>

<template>
  <div class="slot">
    <slot :nameValue="nameValue" :age="ageValue"></slot>  <!-- 親コンポーネントのslotPropsに値を渡している -->
  </div>
</template>
Parent.vue
<script setup>
  import Component from './components/Component.vue'
</script>

<template>
  <Component v-slot="slotProps">  <!-- 子コンポーネントから渡された値を受け取っている -->
    <p>{{ slotProps.nameValue }} - {{ slotProps.age }}</p>
  </Component>
</template>

v-slot="slotProps"v-slot="{ nameValue, age }"として、オブジェクトの分割として受け取ることもできる。

では、次に名前付きスロットの場合どうなるか見ていく。
記述方法は、そこまで変わらない。v-slot:slotName="propsName"とすれば良い。これを省略記法で記述すると#slotName="propsName"となる。

注意したいのは、デフォルトスロットと名前付きスロットを混在させて、スコープスロットを使う場合、デフォルトスロットも<template>タグを使い、その中でスコープスロットの値を使う必要がある。

これは、デフォルトスコープのpropsのスコープが曖昧にならないようにするためである。

sample code
Child.vue
<script setup>
  import { ref } from 'vue'

  const fullName = ref('Riho Yoshioka')
  const age = ref(29)
  const address = ref('Tokyo')
</script>

<template>
  <div class="default">
    <slot :fullName="fullName"></slot>
  </div>
  <div class="age">
    <slot name="age" :age="age"></slot>
  </div>
  <div class="address">
    <slot name="address" :address="address"></slot>
  </div>
</template>
Parent.vue
<script setup>
  import Component from './components/Component.vue'
</script>

<template>
  <Component>
    <template #default="{ fullName }">
      {{ fullName }}
    </template>
    <template #age="{ age }">
      {{ age }}
    </template>
    <template #address="{ address }">
      {{ address }}
    </template>
  </Component>
</template>

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

レンダーレスコンポーネントとは、ロジックと視覚的な出力(スタイリング)の両方をカプセル化したいケースで特に効力を発揮するコンポーネント。つまり、このコンポーネントのテンプレートはほとんどない状態である。

https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCBNb3VzZVRyYWNrZXIgZnJvbSAnLi9Nb3VzZVRyYWNrZXIudnVlJ1xuPC9zY3JpcHQ+XG5cbjx0ZW1wbGF0ZT5cblx0PE1vdXNlVHJhY2tlciB2LXNsb3Q9XCJ7IHgsIHkgfVwiPlxuICBcdE1vdXNlIGlzIGF0OiB7eyB4IH19LCB7eyB5IH19XG5cdDwvTW91c2VUcmFja2VyPlxuPC90ZW1wbGF0ZT4iLCJpbXBvcnQtbWFwLmpzb24iOiJ7XG4gIFwiaW1wb3J0c1wiOiB7XG4gICAgXCJ2dWVcIjogXCJodHRwczovL3NmYy52dWVqcy5vcmcvdnVlLnJ1bnRpbWUuZXNtLWJyb3dzZXIuanNcIlxuICB9XG59IiwiTW91c2VUcmFja2VyLnZ1ZSI6IjxzY3JpcHQgc2V0dXA+XG5pbXBvcnQgeyByZWYsIG9uTW91bnRlZCwgb25Vbm1vdW50ZWQgfSBmcm9tICd2dWUnXG4gIFxuY29uc3QgeCA9IHJlZigwKVxuY29uc3QgeSA9IHJlZigwKVxuXG5jb25zdCB1cGRhdGUgPSBlID0+IHtcbiAgeC52YWx1ZSA9IGUucGFnZVhcbiAgeS52YWx1ZSA9IGUucGFnZVlcbn1cblxub25Nb3VudGVkKCgpID0+IHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdtb3VzZW1vdmUnLCB1cGRhdGUpKVxub25Vbm1vdW50ZWQoKCkgPT4gd2luZG93LnJlbW92ZUV2ZW50TGlzdGVuZXIoJ21vdXNlbW92ZScsIHVwZGF0ZSkpXG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8c2xvdCA6eD1cInhcIiA6eT1cInlcIi8+XG48L3RlbXBsYXRlPiJ9

オクトオクト

Provide / Inject

🐙 親のコンポーネントから子孫コンポーネントとかそれより深いネストされたコンポーネントにプロパティを渡すとき、間にいるコンポーネントにもプロパティを渡さないといけないのは面倒じゃない?

こういうケースに最適なprovideinjectっていう機能を提供しているよ。


Vue.js公式ガイドより

provide

データを提供する側で使う関数がprovide()
第1引数に、子孫コンポーネントで提供された値を参照するための名前にあたる部分。文字列もしくはSymbolで定義する。
複数のprovide()を定義して、その中で違うインジェクションキーを定義すれば複数の値を渡すことができる。

第2引数は、提供される値で、リアクティブオブジェクトも渡すことができる。その場合、親コンポーネントと子孫コンポーネントがリアクティブに接続されていることを意味する。

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

provide('address', 'Tokyo')  // 1. key(=injection key) 2. value
</script>

アプリケーションレベルでデータを提供することもできて、Vueインスタンスに対してprovide関数を定義する。

Inject

祖先コンポーネントから渡されてきた値を受け取る関数はinject()を使う。

sample code
Parent.vue
<script setup>
import { ref, provide } from 'vue'
import Child from './components/Component.vue'

const address = ref('Tokyo')
provide('address', address)
</script>

<template>
  <input type="text" v-model="address">
  <Child />
</template>
Child.vue
<script setup>
import Nested from './Nested.vue'
</script>

<template>
  <Nested />
</template>
GrandChild.vue
<script setup>
  import { inject } from 'vue'

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

<template>
  <h1>My address is {{ address }}</h1>
</template>

injectのデフォルト値

先祖コンポーネントで、インジェクションキーが提供されていない場合、実行時に警告を出す。
デフォルト値をinject()の第2引数に設定することで回避できる。

const address = inject('address', 'default value')

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

TODO: ファクトリー関数とは?

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

リアクティブな値をインジェクトする場合、できるだけプロバイダーコンポーネントの中で、状態の変更を行うべきである。
なぜなら、提供される値の状態と、変更できる場所が同じであることで、将来的なメンテナンス性を高めるためである。

🐙 でも、インジェクターコンポーネントで値を変更したい時もあるよ?

そういうケースは、プロバイダーコンポーネントから値の状態を変更する関数をインジェクターコンポーネントに渡してあげれば良いよ。

Sample code
Parent.vue
<script setup>
import { ref, provide } from 'vue'
import Child from './components/Component.vue'

const address = ref('Tokyo')
const updateAddress = () => {
  address.value = 'Yokohama'
}
provide('address', {
  address,
  updateAddress
})
</script>

<template>
  <input type="text" v-model="address">
  <Child />
</template>
GrandChild.vue
<script setup>
  import { inject } from 'vue'

  const { address, updateAddress } = inject('address')
</script>

<template>
  <h1>My address is {{ address }}</h1>
  <button @click="updateAddress">Change Text</button>
</template>

🐙 あ、でも上記のサンプルコードでは、インジェクターコンポーネントで値を変更する関数を置いて、それを実行させると提供された値を変更できるんだね!

Sample code
インジェクターコンポーネント
<script setup>
  import { inject } from 'vue'

  const { address, updateAddress } = inject('address')
  const updateGrandAddress = () => {
    address.value = 'Okinawa'
  }
</script>

<template>
  <h1>My address is {{ address }}</h1>
  <button @click="updateAddress">Change Text</button>
  <button @click="updateGrandAddress">Change Text</button>  <!-- インジェクターコンポーネントで定義した関数でも値の変更をできる -->
</template>

インジェクターコンポーネントで定義した関数での値の変更を阻止したい場合は、readonly()を使う。
TODO: このとき、複数の値を渡せない?もっと調査が必要。

Sample code
Parent.vue
<script setup>
import { ref, provide, readonly } from 'vue'
import Child from './components/Component.vue'

const address = ref('Tokyo')
const updateAddress = () => {
  address.value = 'Yokohama'
}
provide('address', readonly(address))
provide('updateAddress', updateAddress)
</script>

<template>
  <input type="text" v-model="address">
  <Child />
</template>
GrandChild.vue
<script setup>
  import { inject } from 'vue'

  const address = inject('address')
  const updateAddress = inject('updateAddress')
  const updateGrandAddress = () => {
    address.value = 'Okinawa'
  }
</script>

<template>
  <h1>My address is {{ address }}</h1>
  <button @click="updateAddress">Change Text</button>
  <button @click="updateGrandAddress">Change Text</button>  <!-- リアクティブな値であるaddressは参照のみなので、テキストが更新されることはない -->
</template>
オクトオクト

Composable

VueにおけるComposableとは、CompositionAPIを使って状態を持つロジックをカプセル化し再利用するための関数である。
状態のないロジックをカプセル化し、ある入力を受け取ると即座に期待される出力を返す関数を作ることはよくある。
一方で、状態を持つロジックは、時間と共に変化する状態の管理が伴う。

じゃあ、例を出してみよう。公式から拝借。

あるコンポーネントの中で、マウストラッカー機能を実装すると以下のようになる。

Sample code
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)

function update(event) {
  x.value = event.pageX
  y.value = event.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>
  <p>Mouse Position is at: {{ x }}, {{ y }}</p>
</template>

じゃあ、複数のコンポーネントで上の機能を使いまわしたいとしたら、どうする?
ここで登場してくるのが、Composable関数である。外部ファイルに切り出して、使いたいときにそのファイルを読み込めばOK。

Sample code
import { ref, onMounted, onUnmounted } from 'vue'

// 慣習として、関数名は`use`から始める。
export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }  // 管理された状態を戻り値として外部に公開する。
}
App.vue
<script setup>
import { useMouse } from './components/mouse.js'

const { x, y } = useMouse()
</script>

<template>
  <p>Mouse position is at: {{ x }}, {{ y }}</p>
</template>

もっとすごいことが、ネストすることができる、ということ。
こうすることで、小さくて独立したユニット、単体の関数を使って複雑なロジックを組み立てることができる。

Sample code
event.js
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}
mouse.js
import { ref } from 'vue'
import { useEventListener } from './event.js'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  useEventListener(window, 'mousemove', (event) => {
    x.value = event.pageX
    y.value = event.pageY
  })

  return { x, y }
}

useMouse()を呼び出すコンポーネントの各インスタンス(xy)は、元々の定義された場所(mouse.js)のxyの状態のコピーを独自に作成されるため、コンポーネント間でお互いに干渉することはない。
逆に、コンポーネント間で状態の共有をして管理したい場合は、piniaなどの状態管理ツールを使うべきである。