Vue.js スロットと動的コンポーネント演習

2025-07-30

機能のまとめ

スロット(slot)の基本概念

  1. コンテンツ配信メカニズム
  • 親コンポーネントから子コンポーネントにコンテンツを渡す仕組み
  • コンポーネントの再利用性を高める
  1. 主なスロットタイプ

  <!-- デフォルトスロット -->
  <slot></slot>

  <!-- 名前付きスロット -->
  <slot name="header"></slot>

  <!-- スコープ付きスロット -->
  <slot :item="item"></slot>
  1. 使用例

  <!-- 親コンポーネント -->
  <ChildComponent>
    <template v-slot:header>
      <h1>ヘッダー内容</h1>
    </template>
    メインコンテンツ
  </ChildComponent>

keep-aliveと動的コンポーネント

  1. 動的コンポーネント
  • :is属性で動的にコンポーネントを切り替え
  • コンポーネントインスタンスの再作成が発生
  1. keep-aliveの役割
  • コンポーネントの状態を保持
  • ライフサイクルフックのactivated/deactivatedが使用可能に
  1. 基本構文

  <keep-alive>
    <component :is="currentComponent"></component>
  </keep-alive>

演習問題

初級問題(6問)

スロット関連

  1. デフォルトスロットを使用して、親から子コンポーネントにテキストコンテンツを渡す実装
  2. 名前付きスロット(header, footer)を持つ子コンポーネントを作成
  3. スロットのフォールバックコンテンツを設定

keep-alive関連

  1. 動的コンポーネントの基本的な切り替え実装
  2. keep-aliveでラップした動的コンポーネントの作成
  3. 動的コンポーネント切り替えボタンの実装

中級問題(12問)

スロット関連

  1. スコープ付きスロットで子のデータを親に渡す実装
  2. 動的スロット名を使用したコンポーネント作成
  3. 名前付きスロットの省略記法(#header)を使用
  4. スロットプロップスを使用したデータ通信
  5. 複数のスコープ付きスロットを持つコンポーネント
  6. スロットを使用した高階層コンポーネントパターン

keep-alive関連

  1. keep-aliveのinclude/exclude属性を使用
  2. 動的コンポーネントのトランジション効果追加
  3. activated/deactivatedライフサイクルフックの実装
  4. 動的コンポーネントの状態保持検証
  5. keep-aliveでラップされたコンポーネントの再レンダリング制御
  6. 動的コンポーネントの非同期読み込み

上級問題(6問)

スロット関連

  1. レンダーレスコンポーネント(ロジックのみ提供)の実装
  2. スコープ付きスロットを使用したカスタム反復コンポーネント
  3. スロットを使用したプラグインシステムの実装

keep-alive関連

  1. keep-aliveのカスタム戦略実装(キャッシュ制御)
  2. 動的コンポーネントの遅延読み込みとkeep-aliveの統合
  3. ルートレベルの動的コンポーネント切り替えシステム

Vue.js スロットと動的コンポーネント 全解答

初級問題解答(6問)

解答1: デフォルトスロット


  <!-- ChildComponent.vue -->
  <template>
    <div class="child">
      <slot>デフォルトの表示内容</slot>
    </div>
  </template>

  <!-- ParentComponent.vue -->
  <template>
    <ChildComponent>
      親から渡すコンテンツ
    </ChildComponent>
  </template>

解答2: 名前付きスロット


  <!-- CardComponent.vue -->
  <template>
    <div class="card">
      <slot name="header"></slot>
      <slot></slot>
      <slot name="footer"></slot>
    </div>
  </template>

  <!-- ParentComponent.vue -->
  <template>
    <CardComponent>
      <template v-slot:header>
        <h2>カードタイトル</h2>
      </template>
      メインコンテンツ
      <template v-slot:footer>
        <button>詳細</button>
      </template>
    </CardComponent>
  </template>

解答3: フォールバックコンテンツ


  <!-- AlertComponent.vue -->
  <template>
    <div class="alert">
      <slot>
        <p>デフォルトの警告メッセージ</p>
      </slot>
    </div>
  </template>

解答4: 動的コンポーネント基本


  <template>
    <component :is="currentComponent"></component>
    <button @click="toggleComponent">切り替え</button>
  </template>

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

  export default {
    data() {
      return {
        currentComponent: 'ComponentA'
      }
    },
    components: { ComponentA, ComponentB },
    methods: {
      toggleComponent() {
        this.currentComponent = this.currentComponent === 'ComponentA' 
          ? 'ComponentB' 
          : 'ComponentA'
      }
    }
  }
  </script>

解答5: keep-alive基本


  <template>
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
  </template>

解答6: コンポーネント切り替えボタン


<template>
  <div>
    <button 
      v-for="tab in tabs" 
      :key="tab" 
      @click="currentTab = tab"
    >
      {{ tab }}
    </button>
    <keep-alive>
      <component :is="currentTab"></component>
    </keep-alive>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tabs: ['Home', 'About', 'Contact'],
      currentTab: 'Home'
    }
  },
  components: {
    Home, About, Contact
  }
}
</script>

中級問題解答(12問)

解答7: スコープ付きスロット


<!-- UserList.vue -->
<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <slot :user="user">{{ user.name }}</slot>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, name: '山田太郎', age: 28 },
        { id: 2, name: '佐藤花子', age: 32 }
      ]
    }
  }
}
</script>

<!-- ParentComponent.vue -->
<template>
  <UserList v-slot="{ user }">
    {{ user.name }} ({{ user.age }}歳)
  </UserList>
</template>

解答8: 動的スロット名


<!-- DynamicSlot.vue -->
<template>
  <div>
    <slot :name="slotName"></slot>
  </div>
</template>

<script>
export default {
  props: {
    slotName: {
      type: String,
      default: 'default'
    }
  }
}
</script>

<!-- ParentComponent.vue -->
<template>
  <DynamicSlot :slot-name="currentSlot">
    <template #[currentSlot]>
      動的スロットコンテンツ
    </template>
  </DynamicSlot>
</template>

解答9: スロット省略記法


<template>
  <CardComponent>
    <template #header>
      <h2>省略記法</h2>
    </template>
    <template #default>
      メインコンテンツ
    </template>
    <template #footer>
      <button>OK</button>
    </template>
  </CardComponent>
</template>

解答10: スロットプロップス


<template>
  <div>
    <slot :data="items" :load="loadData"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: []
    }
  },
  methods: {
    async loadData() {
      this.items = await fetchData()
    }
  }
}
</script>

解答11: 複数スコープ付きスロット


<template>
  <table>
    <thead>
      <slot name="header" :columns="columns"></slot>
    </thead>
    <tbody>
      <tr v-for="item in data" :key="item.id">
        <slot name="body" :item="item"></slot>
      </tr>
    </tbody>
  </table>
</template>

解答12: 高階層コンポーネント


<template>
  <div class="enhanced">
    <slot :enhancedData="processedData"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      processedData: transformData(this.originalData)
    }
  },
  props: ['originalData']
}
</script>

解答13: include/exclude


<template>
  <keep-alive :include="['ComponentA', 'ComponentB']">
    <component :is="currentComponent"></component>
  </keep-alive>
</template>

解答14: トランジション効果


<template>
  <transition name="fade" mode="out-in">
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
  </transition>
</template>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

解答15: activated/deactivated

export default {
  activated() {
    console.log('コンポーネントがアクティブになりました')
    this.startPolling()
  },
  deactivated() {
    console.log('コンポーネントが非アクティブになりました')
    this.stopPolling()
  }
}

解答16: 状態保持検証

<!-- CounterComponent.vue -->
<template>
  <div>
    <p>カウント: {{ count }}</p>
    <button @click="count++">増加</button>
  </div>
</template>

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

解答17: 再レンダリング制御


<keep-alive :max="3">
  <router-view></router-view>
</keep-alive>

解答18: 非同期読み込み

const AsyncComponent = () => ({
  component: import('./AsyncComponent.vue'),
  loading: LoadingComponent,
  error: ErrorComponent,
  delay: 200,
  timeout: 3000
})

上級問題解答(6問)

解答19: レンダーレスコンポーネント


<!-- Toggle.vue -->
<script>
export default {
  props: {
    initialValue: Boolean
  },
  data() {
    return {
      value: this.initialValue
    }
  },
  methods: {
    toggle() {
      this.value = !this.value
    }
  },
  render() {
    return this.$scopedSlots.default({
      value: this.value,
      toggle: this.toggle
    })
  }
}
</script>

解答20: カスタム反復コンポーネント


<!-- Iterator.vue -->
<template>
  <div>
    <slot 
      v-for="(item, index) in items" 
      :item="item" 
      :index="index"
      :last="index === items.length - 1"
    ></slot>
  </div>
</template>

解答21: プラグインシステム


// PluginSystem.vue
<template>
  <div>
    <slot :plugins="plugins"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      plugins: []
    }
  },
  methods: {
    register(plugin) {
      this.plugins.push(plugin)
    }
  }
}
</script>

解答22: カスタムキャッシュ戦略

const customCache = new Map()

export default {
  name: 'CustomKeepAlive',
  abstract: true,
  props: {
    max: Number
  },
  created() {
    this.cache = new Map()
  },
  destroyed() {
    for (const [key, vnode] of this.cache) {
      customCache.set(key, vnode)
    }
  },
  render() {
    const slot = this.$slots.default
    const vnode = slot[0]
    if (vnode && vnode.componentOptions) {
      const key = vnode.key ?? vnode.componentOptions.Ctor.cid
      if (this.cache.has(key)) {
        vnode.componentInstance = this.cache.get(key).componentInstance
      } else {
        this.cache.set(key, vnode)
        if (this.max && this.cache.size > parseInt(this.max)) {
          const oldestKey = this.cache.keys().next().value
          this.cache.delete(oldestKey)
        }
      }
      vnode.data.keepAlive = true
    }
    return vnode
  }
}

解答23: 遅延読み込み統合

<template>
  <keep-alive>
    <component :is="loadComponent()"></component>
  </keep-alive>
</template>

<script>
export default {
  methods: {
    loadComponent() {
      return () => ({
        component: import('./HeavyComponent.vue'),
        loading: LoadingComponent
      })
    }
  }
}
</script>

解答24: ルートレベル切り替えシステム

// main.js
import Vue from 'vue'

const app = new Vue({
  data: {
    currentView: 'Home'
  },
  methods: {
    setView(view) {
      this.currentView = view
    }
  },
  render(h) {
    return h('keep-alive', [h(this.currentView)])
  }
}).$mount('#app')

// 使用例
app.setView('About')

この解答セットは、Vue.jsのスロットと動的コンポーネントに関するあらゆる側面を網羅しています。初級問題で基本を理解し、中級問題で実践的なスキルを習得し、上級問題で高度なパターンをマスターできる構成になっています。各解答には実際のプロジェクトで活用できる実用的なコード例を示しており、Vue.jsのコンポーネントシステムを深く理解するのに役立ちます。