1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue 3のpropsをゲームのアイテム欄で整理する

1
Last updated at Posted at 2026-08-02

Vue 3のpropsを、回復薬のアイテム欄で整理する

今日はVue 3のpropsについて、親から子へ値を渡す基本形を確認してから、複数アイテムの一覧表示へ広げてみました。
今回はバッグ内のすべてのアイテムを追加・使用できるようにして、親が更新したデータが子のアイテム欄へどう反映されるかを整理します。

まだ途中段階なので、勘違いなどあれば教えていただけるとうれしいです。

今日やったこと

まずは、親が値を持ち、子がpropsで受け取って表示する基本形を確認しました。

propsは、親コンポーネントから子コンポーネントへ情報を渡すための仕組みです。今回は親をバッグ、子をアイテム欄として考えてみます。親が値を更新すると、子へ渡されるpropsも更新されます。Vue.js公式ドキュメント:props

まずは基本のprops

App.vueを親、ItemStatus.vueを子として、親が持つ回復薬の個数を子へ渡します。

<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import ItemStatus from './components/ItemStatus.vue'

// 親コンポーネントが回復薬の個数を管理する
const potionCount = ref(3)

// 状態を更新する処理も、値を持つ親に置く
const addPotion = () => {
  potionCount.value++
}
</script>

<template>
  <main>
    <h1>バッグ</h1>

    <!--
      item-nameは固定の文字列なので:なし
      item-countは変数の値なので:ありで渡す
    -->
    <ItemStatus
      item-name="回復薬"
      :item-count="potionCount"
    />

    <!-- 親が管理する個数を増やす -->
    <button type="button" @click="addPotion">
      回復薬を1個追加
    </button>
  </main>
</template>

子のItemStatus.vueでは、definePropsで親から届く値を受け取ります。

<!-- components/ItemStatus.vue -->
<script setup>
// 親から渡されるpropsの名前を宣言する
const props = defineProps([
  'itemName',
  'itemCount',
])
</script>

<template>
  <!-- 親から受け取った値を表示に使う -->
  <p>🧪 {{ props.itemName }}:{{ props.itemCount }}個</p>
</template>

この例では、ボタンを押すと親のpotionCount3 → 4 → 5と変わり、子の表示も更新されます。


image.png
クリックすると動作を確かめられます


キャラクター 「親がバッグの中身を持って、子がアイテム欄として表示する。まずは値の置き場所を追えるようになりたいね。」

ゲームのアイテム欄に置き換えると、親はバッグにある回復薬の所持数を管理し、子は回復薬:3個と画面へ表示する役です。

複数アイテムを操作する

次は、回復薬だけではなく、バッグにあるすべてのアイテムを操作できるようにします。

ここではitemsを「今バッグに入っているアイテム」、itemCatalogを「追加できるアイテムの一覧」として親で持たせます。0個になったアイテムはitemsから削除し、追加ボタンを押した場合はカタログからバッグへ戻す形にしました。

<!-- App.vue -->
<script setup>
import { computed, ref } from 'vue'
import ItemStatus from './components/ItemStatus.vue'

// 親が管理する、現在バッグに入っているアイテム一覧
const items = ref([
  {
    itemId: 1,
    itemName: '回復薬',
    itemCount: 3,
    rarity: '一般',
  },
  {
    itemId: 2,
    itemName: '魔力薬',
    itemCount: 2,
    rarity: '希少',
  },
  {
    itemId: 3,
    itemName: '解毒薬',
    itemCount: 1,
    rarity: '一般',
  },
  {
    itemId: 4,
    itemName: '万能薬',
    itemCount: 1,
    rarity: '貴重',
  },
])

// 一覧から消えたアイテムを追加し直すための元データ
const itemCatalog = [
  {
    itemId: 1,
    itemName: '回復薬',
    rarity: '一般',
  },
  {
    itemId: 2,
    itemName: '魔力薬',
    rarity: '希少',
  },
  {
    itemId: 3,
    itemName: '解毒薬',
    rarity: '一般',
  },
  {
    itemId: 4,
    itemName: '万能薬',
    rarity: '貴重',
  },
]

// 親が管理している、現在のアイテム種類数を計算する
const itemTypeCount = computed(() => items.value.length)

// 指定したIDのアイテムを1個追加する
const addItem = (itemId) => {
  // バッグにすでにあるアイテムを探す
  const existingItem = items.value.find((item) => item.itemId === itemId)

  if (existingItem) {
    // ある場合は個数だけ増やす
    existingItem.itemCount++
    return
  }

  // 0個で消えている場合は、カタログから元データを探す
  const catalogItem = itemCatalog.find((item) => item.itemId === itemId)

  if (catalogItem) {
    // 新しいオブジェクトとしてバッグの配列へ追加する
    items.value.push({
      ...catalogItem,
      itemCount: 1,
    })
  }
}

// 指定したIDのアイテムを1個使う
const useItem = (itemId) => {
  // バッグから対象のアイテムを探す
  const targetItem = items.value.find((item) => item.itemId === itemId)

  // アイテムが存在しない場合は何もしない
  if (!targetItem) {
    return
  }

  // 親が管理している個数を減らす
  targetItem.itemCount--

  // 0個になった場合は、親の配列からアイテムを取り除く
  if (targetItem.itemCount === 0) {
    items.value = items.value.filter((item) => item.itemId !== itemId)
  }
}
</script>

<template>
  <main>
    <h1>バッグ</h1>

    <!-- 親が管理しているアイテム種類数 -->
    <p>所持アイテム:{{ itemTypeCount }}種類</p>

    <!-- 親が持つ配列の数だけ、アイテム欄と操作ボタンを表示する -->
    <ul class="item-list">
      <li v-for="item in items" :key="item.itemId" class="item-list__item">
        <!-- 子には表示に必要な値だけをpropsとして渡す -->
        <ItemStatus
          :item-name="item.itemName"
          :item-count="item.itemCount"
          :rarity="item.rarity"
        />

        <!-- 状態を更新する操作は親に置く -->
        <div class="item-list__actions">
          <button type="button" @click="addItem(item.itemId)">
            1個追加
          </button>

          <button type="button" @click="useItem(item.itemId)">
            1個使う
          </button>
        </div>
      </li>
    </ul>

    <section class="catalog">
      <h2>アイテムを追加する</h2>

      <!-- 一覧から消えたアイテムも、親の操作でバッグへ戻せる -->
      <button
        v-for="item in itemCatalog"
        :key="item.itemId"
        type="button"
        @click="addItem(item.itemId)"
      >
        {{ item.itemName }}を追加
      </button>
    </section>
  </main>
</template>

<style scoped>
.item-list {
  /* リストの標準の余白と黒丸をなくす */
  padding: 0;
  list-style: none;
}

.item-list__item {
  /* アイテム欄と操作ボタンをまとめる */
  margin-bottom: 0.75rem;
}

.item-list__actions {
  /* 各アイテムの追加・使用ボタンを横に並べる */
  display: flex;
  gap: 0.5rem;

  /* アイテム欄との間隔 */
  margin-top: 0.25rem;
}

.catalog {
  /* アイテム追加エリアを一覧から分ける */
  margin-top: 1.25rem;
}

button {
  /* 文字量に合わせた横幅 */
  width: fit-content;

  /* ボタン内の余白 */
  padding: 0.4rem 0.8rem;

  /* ボタン同士の間隔 */
  margin-right: 0.5rem;
  margin-bottom: 0.5rem;

  /* クリック可能だと分かりやすくする */
  cursor: pointer;
}
</style>

アイテム欄を表示する子は、最初の基本形から少し広げて、レアリティも受け取ります。

<!-- components/ItemStatus.vue -->
<script setup>
// 親から受け取るpropsの型と条件を定義する
const props = defineProps({
  // アイテム名は必須の文字列
  itemName: {
    type: String,
    required: true,
  },

  // 個数は必須の数値で、0より小さくならないことを想定する
  itemCount: {
    type: Number,
    required: true,
    validator: (value) => value >= 0,
  },

  // レアリティが渡されない場合は一般として表示する
  rarity: {
    type: String,
    default: '一般',
  },
})
</script>

<template>
  <article class="item-status">
    <div>
      <!-- 親から渡されたアイテム名とレアリティを表示する -->
      <span class="item-status__name">
        🧪 {{ props.itemName }}
      </span>

      <span class="item-status__rarity">
        {{ props.rarity }}
      </span>
    </div>

    <!-- 親から渡された個数を表示する -->
    <strong class="item-status__count">
      {{ props.itemCount }}個
    </strong>
  </article>
</template>

<style scoped>
.item-status {
  /* アイテム名と個数を左右に配置する */
  display: flex;
  justify-content: space-between;
  align-items: center;

  /* アイテム欄らしい余白と区切り線 */
  padding: 0.6rem 0.75rem;
  border-bottom: 1px solid #ddd;
}

.item-status__rarity {
  /* アイテム名との間隔 */
  margin-left: 0.5rem;

  /* 補足情報として少し小さくする */
  font-size: 0.85rem;
  color: #666;
}
</style>

image.png
クリックすると動作を確かめられます


v-forは配列のデータをもとに一覧を描画するため、親のitems配列からアイテムが消えると、対応するアイテム欄と操作ボタンも画面から消えます。Vue.js公式ドキュメント:リストレンダリング vueframework

キャラクター 「どのアイテムも同じ追加・使用の流れで操作できると、親の配列が変わって、一覧表示も変わるところを見比べやすいね。」

つまずいたところ

:の有無、props名の書き方、子がpropsを更新してよいかが最初は混ざっていました。

:あり・なしの違い

固定文字列と、変数に入っている値では渡し方が異なります。

<template>
  <!-- 固定文字列を渡すので:なし -->
  <ItemStatus item-name="回復薬" />

  <!-- item.itemCountの数値を渡すので:あり -->
  <ItemStatus :item-count="item.itemCount" />

  <!-- item.itemCountという文字列が渡る -->
  <ItemStatus item-count="item.itemCount" />
</template>

:v-bind:の省略形で、変数や式を評価した結果を渡します。個数・選択中のID・検索条件のように変わる値は、:を付けて渡します。Vue.js公式ドキュメント:テンプレート構文

キャラクター 「回復薬の“今の個数”を渡したいなら:が必要。文字そのものを渡したい場合とは分けて覚えたいね。」

命名規則

propsは、子のJavaScript側でcamelCase、親のテンプレート側でkebab-caseを使う形が推奨されています。Vue.js公式ドキュメント:propsの命名規則

場所 書き方
子の<script setup> camelCase itemNameitemCount
親の<template> kebab-case item-nameitem-count
v-bindのオブジェクト camelCase itemName: item.itemName

item-nameitemNameは別のpropsではありません。テンプレートで渡すときはハイフン区切り、JavaScript側で受け取るときは大文字区切り、と今は整理しています。

子はpropsを直接変えない

propsは親から子へ下りる値なので、子で直接書き換えない形が基本です。今回も追加・使用・削除の処理は親のApp.vueへ置き、子のItemStatus.vueは表示だけを担当させています。

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

// ❌ 子が受け取ったpropsを直接書き換えない
// props.itemCount--
</script>

アイテム欄はバッグそのものではなく、バッグの内容を表示する部品です。次に子のカードへ「使う」ボタンを置くなら、defineEmitsで親へアイテムIDを伝え、親が更新する流れを試したいです。

今の理解(メモ)

まだ細かい設計の判断は迷いますが、propsは親が持つデータを子の画面部品へ渡す入口として見えています。

  • 親コンポーネントは、refなどで状態を管理する
  • propsは親から子へ渡す情報で、子は受け取った値を表示に使う
  • defineProps(['itemName'])のように、props名だけを宣言する基本形がある
  • オブジェクト形式のdefinePropsでは、型・必須指定・初期値・validatorを設定できる
  • 固定文字列は:なし、変数や式の結果は:ありで渡す
  • v-forを使うと、親が持つ配列の数に応じてアイテム欄を並べられる
  • 親が配列を更新・削除すると、子の一覧表示にも増減が反映される
  • v-forでは、各データを識別する一意なkeyを指定する
  • propsは子から直接変更せず、値の更新は親へ寄せる

ゲームのアイテム欄では、バッグの中身を持つ親が「管理画面」、子が各アイテムの「表示枠」のように見えます。画像解析結果の一覧なら、親が解析データを管理し、子が画像名・検出数・判定結果を表示する形に置き換えられそうです。

次にやりたいこと

今回は親のボタンで全アイテムを操作したので、次は子の操作を親へ伝える形を確認したいです。

  • itemCountへ文字列や負の数を渡し、型指定とvalidatorの警告をConsoleで確認する
  • itemsの1件からitemNameを外し、配列データの条件をどこまで確認するか考える
  • ItemStatus.vueへ「使う」ボタンを置き、defineEmitsitemIdを親へ渡す
  • 親が受け取ったitemIdを使い、今回のuseItemを呼び出す
  • アイテムが0個になったときに「使う」ボタンが押せなくなる表示も試す
キャラクター 「今は親の操作で十分追えるけれど、次は各アイテム欄から“使ったよ”と親へ知らせる形を試したい。そこでpropsとイベントの違いも見えてきそう。」

今日の整理

親が配列を持ち、子へpropsとして渡す流れを、バッグの操作と合わせて整理します。

要素 役割 回復薬の例 次に確認したいこと
App.vue 状態を管理し、子へ渡す親 バッグの中身と追加・使用処理 更新処理をどこまで親に置くか
ItemStatus.vue propsを受け取って表示する子 アイテム欄の1行 子に操作ボタンを置く
defineProps 子が受け取る値を宣言する 名前・個数・レアリティ TypeScriptでの型定義
: / v-bind 変数や式の値を渡す 現在のアイテム個数 固定文字列との使い分け
v-for 配列の数に応じて一覧を表示する バッグ内のアイテム欄 keyと削除時の更新
items 親が管理する一覧データ バッグの中身 追加・更新・削除の扱い
validator 値の条件を確認する 個数が0以上か確認する Consoleの警告内容
defineEmits 子の操作を親へ知らせる カードの「使う」を親へ伝える アイテムIDを渡す形

おわりに

今回は、propsの基本形から始めて、複数アイテムを操作するバッグの一覧へ広げてみました。すべてのアイテムで同じ追加・使用処理を使う形にすると、親のitems配列が変わり、その内容が子のアイテム欄へ反映される流れを追いやすくなりました。

まだpropsの数が増えたときの分け方や、子へ操作ボタンを置く場面は迷いそうです。ただ、画像解析結果を一覧表示して、データの追加・更新・削除を画面へ反映する仕組みにもつなげられそうです。次はdefineEmitsを使い、子の操作を親の状態更新へつなげてみたいです。

学習中の内容なので、勘違いや理解不足もあると思います。もし気になる点があれば教えてもらえるとうれしいです。

 注意事項
 本ブログに掲載している内容は、私個人の見解であり、
 所属する組織の立場や戦略、意見を代表するものではありません。
 あくまでエンジニアとしての経験や考えを発信していますのでご了承ください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?