0
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?

Vue3の算出プロパティcomputedのgeterとseter

Last updated at Posted at 2025-08-12

Vue 3: 親→子→孫での双方向データ共有(computed + defineModel)ガイド

このメモは、親 → 子 → 孫 の階層で値を双方向に扱うときの、最小で安全な設計パターンをまとめたものです。
要点は 「親が単一の真実源(SSOT)」子・孫は defineModel でそのまま受け渡し です。


結論(認識の確認)

親
comptedでget set でプロパティ
↓
子
defineModal で双方向
↓
孫
defineModal で双方向
  • computed({ get, set })状態(例:mealItems[...])に直結したプロパティを作り、v-model:xxx="そのcomputed" で子へ渡す。
  • defineModel('xxx')親の v-model を双方向で受け取る。そのまま孫へも中継可能。
  • defineModel('xxx')入力 UI を v-model="xxx" に結び、編集が親まで一直線に伝播する。

キー名(例:currentCustomNoteselectedOptions など)は 親・子・孫で一致 させてください。


極小サンプル

Parent.vue

<template>
  <!-- 親の computed を v-model で子へ -->
  <Child v-model:value="valueProxy" />
</template>

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

// 単一の真実源(SSOT)
const state = reactive({ value: 'A' })

// 子に渡す v-model ブリッジ(SSOTに直結)
const valueProxy = computed({
  get: () => state.value,
  set: (v) => { state.value = v }
})
</script>

Child.vue

<template>
  <!-- 孫へ同じキー名で中継 -->
  <Grand v-model:value="value" />
</template>

<script setup>
// 親の v-model:value をそのまま受ける(双方向)
const value = defineModel('value', { default: '' })
</script>

Grand.vue

<template>
  <!-- 入力UIは普通に v-model -->
  <input v-model="value" />
</template>

<script setup>
// 親まで届く双方向の値
const value = defineModel('value', { default: '' })
</script>

なぜこの形が良いのか

  • 単一の真実源(SSOT):実データは親が持つ。追跡や保存ロジックが明快。
  • defineModel:親子間の prop + emit('update:...') を自動生成。ボイラープレート激減。
  • 拡張しやすい:子で変換が必要なら、computed ラッパーで加工してから孫へ渡せる。
// 例:子で値を加工して孫へ
const raw = defineModel('value', { default: '' })
const bridged = computed({
  get: () => normalize(raw.value),
  set: (v) => { raw.value = denormalize(v) }
})
// <Grand v-model:value="bridged" />

実務メモ(今回の構成に寄せて)

  • currentCustomNote / selectedOptions / currentMainFood / currentSideFood など、
    親は computed(get/set) で mealItems や snackItem に直結
    子・孫は defineModel('同じキー名') で双方向。
  • KeypadModal を子内で完結させると親がスリムになる(showKeypad / openKeypad 等の状態を親から撤去)。
  • チップ(選択肢)と自由入力の 結合は保存直前に親で行う と二重ロジックを避けられる。
// 例:保存前に結合
const composeNote = (selectedOpts = [], freeText = '') => {
  const chips = (selectedOpts || []).map(o => o.label).join('')
  const free  = (freeText || '').trim()
  if (chips && free) return `${chips}${free}`
  if (chips) return chips
  return free
}

ありがちなハマりどころ

  • キー名不一致v-model:xxxdefineModel('xxx')xxx が違う と双方向にならない。
  • Vue バージョンdefineModelVue 3.3+ が必要(ビルド環境が3.3以上なら本番のサーバー側は関係なし)。
  • 相対パスparts/ からの相対 import を間違えやすい。例:../../KeypadModal.vue

まとめ

  • 親:computed(get/set) で SSOT に直結 → 子へ v-model で渡す。
  • 子・孫:defineModel で双方向を素直に受け、必要ならそのまま中継。
  • 複雑な加工はどこか1か所(たとえば親の保存直前)に寄せて、二重ロジックを排除

これで親→子→孫の v-model チェーンがシンプル&堅牢に動きます。

Vue 3(<script setup>)での defineProps / defineEmits / defineModel の役割と違い

まとめ:

  • defineProps … 親から 受け取る 値の宣言(下りの一方向 / 読み取り専用)
  • defineEmits … 親へ 通知する イベントの宣言(上りの一方向)
  • defineModelv-model 専用の糖衣構文。対象の propupdate イベントまとめて 作り、読み書きできる Ref を返す

1. defineProps:親からもらう値(下り)

<script setup lang="ts">
// 親から来る props を宣言(読み取り専用)
const props = defineProps<{
  options: string[]
  disabled?: boolean
}>()
</script>
  • props は 読み取り専用props.options.push(...) のように 直接変更しない
  • 変更したいときは 親にイベントで知らせる(= defineEmits)か、v-model を使う。

2. defineEmits:親へ知らせるイベント(上り)

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'select', value: string): void
  (e: 'close'): void
}>()

const onClick = (v: string) => emit('select', v)
</script>
  • 子→親に「何が起きたか」を伝える。
  • emit('イベント名', payload) の型/存在をここで宣言する。

3. defineModel(Vue 3.3+):v-model を一行で扱う

<script setup lang="ts">
// v-model:selected を受け取り・更新できる Ref を返す
const modelSelected = defineModel<string[]>('selected', { default: [] })

// デフォルトの v-model(= modelValue / update:modelValue)なら引数省略可
const modelValue = defineModel<string>({ default: '' })
</script>
  • 内部で selected prop と update:selected イベントを生成し、Ref を返す。
  • つまり次の 従来コードと同等:
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{ selected: string[] }>()
const emit  = defineEmits<{ (e:'update:selected', v:string[]):void }>()

const modelSelected = computed({
  get: () => props.selected,
  set: (v) => emit('update:selected', v),
})
</script>

注意defineModel('selected') を使う場合、同じキー selecteddefineProps で重複宣言しない
その v-model 対象に関しては defineEmitsupdate:selected を別途宣言する必要もない
(他の通常 props や他のイベントは従来どおり defineProps / defineEmits を使う)


4. いつ何を使う?(早見表)

目的 使うもの 備考
親から値をもらう(読み取りのみ) defineProps props は直接変更しない
親へイベント通知を送りたい defineEmits emit('done', payload) など
親と双方向に値を連携したい defineModel v-model 対象に限る
v-model を手作りしたい(3.2以前等) defineProps + defineEmits + computed(get/set) defineModel の等価実装

5. 親→子→孫の最小例(defineModel 版)

Parent.vue

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

const selected = ref<string[]>([]) // 親が唯一の state
const options = ['A','B','C']
</script>

<template>
  <Child v-model:selected="selected" :options="options" />
  <pre>Parent: {{ selected }}</pre>
</template>

Child.vue

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

// v-model:selected を 1行で橋渡し
const modelSelected = defineModel<string[]>('selected', { default: [] })
const { options } = defineProps<{ options: string[] }>()
</script>

<template>
  <Grandchild v-model:selected="modelSelected" :options="options" />
</template>

Grandchild.vue

<script setup>
const modelSelected = defineModel<string[]>('selected', { default: [] })
const { options } = defineProps<{ options: string[] }>()

const toggle = (o: string) => {
  const next = [...modelSelected.value]
  const i = next.indexOf(o)
  i === -1 ? next.push(o) : next.splice(i, 1)
  modelSelected.value = next // ← 親まで反映
}
</script>

<template>
  <button v-for="o in options" :key="o" @click="toggle(o)">
    {{ o }} {{ modelSelected.includes(o) ? '' : '' }}
  </button>
</template>

6. よくある落とし穴

  • props を直接変更しないprops.xxx.push(...) ではなく、必ず emit or v-model の setter を使う
  • defineModel と同名の defineProps を併用しない(重複宣言)
  • 複数の v-modeldefineModel('foo'), defineModel('bar') と複数呼び出し可能
  • デフォルト v-modeldefineModel()(引数なし)は modelValue / update:modelValue に対応

結論

  • defineProps:下りの宣言(受け取り)
  • defineEmits:上りの宣言(通知)
  • defineModel:v-model に限り 上り下りを一本化して扱うための便利機能(Ref が返る)

実務では、通常の props / イベントはそのままフォーム値などは defineModel で簡潔に、がスッキリしておすすめです。

超シンプル版:Vue 3 の computed(get/set)親→子→孫 をつなぐ v-model

ゴール:親だけが state を持ち、子と孫は編集だけ通知する最小実装。
例:A/B/C の3つのタグを選択するシンプル UI。


ディレクトリ(例)

src/
├─ Parent.vue
├─ Child.vue
└─ Grandchild.vue

1) 親:Parent.vue

  • 単一の真実源(state)は親だけが持つ:selected(配列)
  • 子へは v-model:selected として渡す
  • 親の selected が変わると UI(<pre>)も更新
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

// 親だけが持つ本体の state
const selected = ref([]) // 例: ['A', 'C'] のように入る

// 孫の表示用候補(例)
const options = ['A', 'B', 'C']
</script>

<template>
  <div style="display:grid; gap:12px;">
    <h2>Parent</h2>
    <Child v-model:selected="selected" :options="options" />

    <div>
      <strong>親の state:</strong>
      <pre>{{ selected }}</pre>
    </div>
  </div>
</template>

2) 子:Child.vue

  • state は持たない
  • 親からの v-model:selectedcomputed(get/set) で橋渡し
  • 孫へも v-model:selected でそのまま渡す
<script setup>
import { computed } from 'vue'
import Grandchild from './Grandchild.vue'

const props = defineProps({
  selected: { type: Array, default: () => [] }, // 親から v-model で渡ってくる
  options:  { type: Array, default: () => [] }
})
const emit = defineEmits(['update:selected'])

// v-model ブリッジ:子は編集だけ親へ通知
const modelSelected = computed({
  get: () => props.selected ?? [],
  set: (val) => emit('update:selected', val)
})
</script>

<template>
  <div style="display:grid; gap:8px;">
    <h3>Child</h3>
    <Grandchild :options="options" v-model:selected="modelSelected" />
  </div>
</template>

3) 孫:Grandchild.vue

  • 実際の UI(トグル)を提供
  • 配列の変更は modelSelected.value = next のみ(props を直接いじらない)
<script setup>
import { computed } from 'vue'

const props = defineProps({
  selected: { type: Array, default: () => [] }, // 子から v-model
  options:  { type: Array, default: () => [] }
})
const emit = defineEmits(['update:selected'])

// 孫でも v-model ブリッジ
const modelSelected = computed({
  get: () => props.selected ?? [],
  set: (val) => emit('update:selected', val)
})

// クリックで配列をトグル
const toggle = (opt) => {
  const next = [...modelSelected.value]
  const i = next.indexOf(opt)
  if (i === -1) next.push(opt)
  else next.splice(i, 1)
  modelSelected.value = next // ← ここが唯一の更新ポイント(親まで伝播)
}
</script>

<template>
  <div style="display:flex; gap:8px; flex-wrap:wrap;">
    <button
      v-for="opt in options" :key="opt"
      @click="toggle(opt)"
      :style="{
        padding:'6px 10px',
        border:'1px solid #ccc',
        borderRadius:'8px',
        background: modelSelected.includes(opt) ? '#16a34a' : '#fff',
        color: modelSelected.includes(opt) ? '#fff' : '#333',
        cursor:'pointer'
      }"
    >
      {{ opt }}
    </button>
  </div>

  <div style="margin-top:8px;">
    <small>孫が受けている値: {{ modelSelected }}</small>
  </div>
</template>

動き(ほんとにシンプルに)

  1. でボタンをクリック → toggle()modelSelected.value = next をセット
  2. 孫の modelSelected.set(next) が発火 → emit('update:selected', next)
  3. modelSelected.set(next) が発火 → emit('update:selected', next)
  4. v-model:selected が受け取り、selected(親の state)が更新
  5. 親の state が下り直し、孫の表示が更新

どの層でも props を直接変更しない更新は常に set → emit で上に伝える、が鉄則。


おまけ:Vue 3.3+ なら defineModel でさらに短く

子・孫で:

<script setup>
const modelSelected = defineModel('selected', { type: Array, default: () => [] })
</script>

これで computed(get/set) のブリッジを省略できます。振る舞いは同じです。


これで 最小の親→子→孫の v-model 伝播が理解できます。
実案件では、親の保存処理だけで文字列結合などのビジネスロジックを行い、子・孫は「編集だけ通知」に徹すると安定します。

Vue 3: computed の getter/setter を使った 親→子→孫v-model ブリッジ入門

目的:親だけが状態を持ち、子と孫は「編集だけ通知」するクリーンなデータフローを作る。
手段:各層で computed({ get, set }) を使って v-model を受け取り/橋渡しする。


ディレクトリ構成(例)

src/
├─ Parent.vue            # 親: 単一の真実源(状態の本体を保持)
├─ Child.vue             # 子: 親⇄孫の橋渡し。state は持たない
└─ Grandchild.vue        # 孫: UI。ユーザー操作→親へ伝播

完成イメージ(データの流れ)

(ユーザー操作) → [孫] modelSelectedOptions.set(next)
                     └─ emit('update:selectedOptions', next)
                        ↓
                 [子]  modelSelectedOptions.set(next)
                     └─ emit('update:selectedOptions', next)
                        ↓
                 [親]  activeSelectedOptions.set(next)  ← 真の状態を更新
                        ↑
         (タブ切替・初期読込などで get が呼ばれ配列が下りてくる)

1) 親: Parent.vue

  • 状態は 親だけが持つ(単一の真実源)。
  • タブ(例:朝/昼/夕/おやつ)に応じて返す配列を切り替えるため、activeSelectedOptionscomputed(get/set) で実装。
  • 子へは v-model:selectedOptions として渡す。
<script setup>
import { ref, computed } from 'vue'
import Child from './Child.vue'

// タブ
const activeMealTime = ref('')

// 真の状態(親だけが保持)
const selectedOptions = ref({
  ActiveOption: [], // 朝/昼/夕 用の表示中配列
  : [], : [], : []
})
const snackSelectedOptions = ref([]) // おやつ用

// 子へ渡す v-model のブリッジ(朝/昼/夕/おやつで配列を切り替え)
const activeSelectedOptions = computed({
  get: () => activeMealTime.value === 'おやつ'
    ? snackSelectedOptions.value
    : selectedOptions.value.ActiveOption,
  set: (val) => {
    if (activeMealTime.value === 'おやつ') {
      snackSelectedOptions.value = val
    } else {
      selectedOptions.value.ActiveOption = val
    }
  }
})

// UI 用(例)
const templateOptions = [
  { id: 1, label: '特変なし' },
  { id: 2, label: '完食' },
  { id: 3, label: 'むせあり' },
]
</script>

<template>
  <div>
    <div>
      <button @click="activeMealTime = '朝'"></button>
      <button @click="activeMealTime = '昼'"></button>
      <button @click="activeMealTime = '夕'"></button>
      <button @click="activeMealTime = 'おやつ'">おやつ</button>
    </div>

    <!-- 子へは配列そのものを v-model で渡す -->
    <Child
      :templateOptions="templateOptions"
      v-model:selectedOptions="activeSelectedOptions"
    />

    <pre>親の状態: {{ activeSelectedOptions }}</pre>
  </div>
</template>

2) 子: Child.vue

  • 状態は持たないcomputed(get/set) で「孫との橋渡し」をするだけ。
  • 孫へも v-model:selectedOptions で渡す。
<script setup>
import { computed } from 'vue'
import Grandchild from './Grandchild.vue'

const props = defineProps({
  // 親から v-model:selectedOptions で渡ってくる
  selectedOptions: { type: Array, default: () => [] },
  templateOptions: { type: Array, default: () => [] }
})
const emit = defineEmits(['update:selectedOptions'])

// 親→子の v-model を孫へ橋渡し(子は state を持たない)
const modelSelectedOptions = computed({
  get: () => props.selectedOptions ?? [],
  set: (val) => emit('update:selectedOptions', val)
})
</script>

<template>
  <Grandchild
    :templateOptions="templateOptions"
    v-model:selectedOptions="modelSelectedOptions"
  />
</template>

3) 孫: Grandchild.vue

  • 実際の UI(チップのトグルなど)を提供。
  • 配列の編集は modelSelectedOptions.value = next とし、set が発火して親へ伝播。
<script setup>
import { computed } from 'vue'

const props = defineProps({
  selectedOptions: { type: Array, default: () => [] },
  templateOptions: { type: Array, default: () => [] }
})
const emit = defineEmits(['update:selectedOptions'])

// v-model ブリッジ(孫でも同じパターン)
const modelSelectedOptions = computed({
  get: () => props.selectedOptions ?? [],
  set: (val) => emit('update:selectedOptions', val)
})

// チップのトグル(例)
const toggle = (opt) => {
  const next = [...modelSelectedOptions.value]
  const i = next.findIndex(o => o.id === opt.id)
  if (i === -1) next.push(opt)
  else next.splice(i, 1)
  modelSelectedOptions.value = next   // ← ここが唯一の更新ポイント
}
</script>

<template>
  <div style="display:flex; gap:8px; flex-wrap:wrap;">
    <button
      v-for="opt in templateOptions" :key="opt.id"
      @click="toggle(opt)"
      :style="{padding:'6px 10px', border:'1px solid #ccc', borderRadius:'8px',
               background: modelSelectedOptions.some(o => o.id===opt.id) ? '#16a34a' : '#fff',
               color: modelSelectedOptions.some(o => o.id===opt.id) ? '#fff' : '#333'}"
    >
      {{ opt.label }}
    </button>
  </div>

  <pre>孫の受取値: {{ modelSelectedOptions }}</pre>
</template>

ポイントまとめ

  • 単一の真実源:状態は親だけが持つ。子/孫は computed({get,set}) でブリッジするだけ。
  • props の直接変更禁止props.xxx.push(...) のように書かない。常に setter を通す
  • v-model の規約v-model:foofoo prop を受け取り、update:foo を emit する。
  • 双方向の役割分担
    • 下り(親→孫):get が呼ばれて props として届く → UI 更新
    • 上り(孫→親):ユーザー操作で set を呼ぶ → emit で伝播 → 親状態が更新

付録:Vue 3.3+ の defineModel でさらに簡潔に

  • 子や孫では次のように書けます(内部で computed を生成してくれるシュガーシンタックス)。
<script setup>
// 子/孫いずれでも
const modelSelectedOptions = defineModel('selectedOptions', { type: Array, default: () => [] })
</script>

<template>
  <!-- 以降は同じ -->
</template>

ただし「親でタブに応じて違う配列を返す/保存する」といったビジネスロジックは親の computed(get/set) が最適です。


以上。これを雛形にすれば、親のみ状態保持・子孫は編集通知だけという安全な設計をシンプルに実現できます。

0
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
0
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?