Meituan-Dianping/mpvue · warning

<transition-group> children must be keyed: <${name}>

Error message

<transition-group> children must be keyed: <${name}>

What it means

<transition-group> implements FLIP-based move animations that require tracking each child by its `key`. During render it builds a children map keyed by `c.key`; if a child has no key it cannot be tracked, so in development Vue warns that transition-group children must be keyed, printing the child's component or tag name.

Source

Thrown at src/platforms/web/runtime/components/transition-group.js:54

  render (h: Function) {
    const tag: string = this.tag || this.$vnode.data.tag || 'span'
    const map: Object = Object.create(null)
    const prevChildren: Array<VNode> = this.prevChildren = this.children
    const rawChildren: Array<VNode> = this.$slots.default || []
    const children: Array<VNode> = this.children = []
    const transitionData: Object = extractTransitionData(this)

    for (let i = 0; i < rawChildren.length; i++) {
      const c: VNode = rawChildren[i]
      if (c.tag) {
        if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {
          children.push(c)
          map[c.key] = c
          ;(c.data || (c.data = {})).transition = transitionData
        } else if (process.env.NODE_ENV !== 'production') {
          const opts: ?VNodeComponentOptions = c.componentOptions
          const name: string = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag
          warn(`<transition-group> children must be keyed: <${name}>`)
        }
      }
    }

    if (prevChildren) {
      const kept: Array<VNode> = []
      const removed: Array<VNode> = []
      for (let i = 0; i < prevChildren.length; i++) {
        const c: VNode = prevChildren[i]
        c.data.transition = transitionData
        c.data.pos = c.elm.getBoundingClientRect()
        if (map[c.key]) {
          kept.push(c)
        } else {
          removed.push(c)
        }
      }
      this.kept = h(tag, null, kept)

View on GitHub (pinned to 6c5d78ee04)

Solutions

  1. Add a unique `:key` to every child rendered inside <transition-group>, e.g. `:key="item.id"` on the v-for element.
  2. Use a stable unique identifier (id from data), not the array index, so move/remove animations track correctly.
  3. Key any hardcoded/conditional children too (e.g. `<span key="label">`).
  4. If the list is truly static and needs no animation, use a plain container instead of transition-group.

Example fix

// before
<transition-group name="list">
  <li v-for="item in items">{{ item.name }}</li>
</transition-group>
// after
<transition-group name="list">
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>
</transition-group>
Defensive patterns

Strategy: validation

Validate before calling

// before rendering, verify every list item has a key source
items.forEach((item, i) => {
  if (item.id == null) console.warn('transition-group child at index ' + i + ' has no unique id for :key')
})

Type guard

const hasKey = c => c && c.key != null
if (!children.every(hasKey)) console.warn('all transition-group children need :key')

Prevention

When it happens

Trigger: Rendering a list inside <transition-group> with v-for but no `:key` binding; rendering static, unkeyed elements as direct children of transition-group; v-for over objects where the key expression is undefined for some items.

Common situations: Forgetting `:key="item.id"` when adding transition-group to an existing v-for; keys bound to the loop index and then removed during refactors; conditional children (v-if) without keys inside a transition-group.

Related errors


AI-assisted analysis of Meituan-Dianping/mpvue@6c5d78ee04 (2026-09-02). Data as JSON: /api/errors/6cc34d7b6ae14161. Report an issue: GitHub.