Repository: vuejs/vue
Pull request: fix(patch): warn error when children with duplicate key

fix #7199

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**


Changed files:
filename: src/core/vdom/patch.js
patch: @@ -263,6 +263,9 @@ export function createPatchFunction (backend) {
 
   function createChildren (vnode, children, insertedVnodeQueue) {
     if (Array.isArray(children)) {
+      if (process.env.NODE_ENV !== 'production') {
+        checkDuplicateKeys(children)
+      }
       for (let i = 0; i < children.length; ++i) {
         createElm(children[i], insertedVnodeQueue, vnode.elm, null, true)
       }
@@ -394,6 +397,10 @@ export function createPatchFunction (backend) {
     // during leaving transitions
     const canMove = !removeOnly
 
+    if (process.env.NODE_ENV !== 'production') {
+      checkDuplicateKeys(newCh)
+    }
+
     while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
       if (isUndef(oldStartVnode)) {
         oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
@@ -426,13 +433,6 @@ export function createPatchFunction (backend) {
           createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
         } else {
           vnodeToMove = oldCh[idxInOld]
-          /* istanbul ignore if */
-          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
-            warn(
-              'It seems there are duplicate keys that is causing an update error. ' +
-              'Make sure each v-for item has a unique key.'
-            )
-          }
           if (sameVnode(vnodeToMove, newStartVnode)) {
             patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
             oldCh[idxInOld] = undefined
@@ -453,6 +453,24 @@ export function createPatchFunction (backend) {
     }
   }
 
+  function checkDuplicateKeys (children) {
+    const seenKeys = {}
+    for (let i = 0; i < children.length; i++) {
+      const vnode = children[i]
+      const key = vnode.key
+      if (isDef(key)) {
+        if (seenKeys[key]) {
+          warn(
+            `Duplicate keys detected: '${key}'. This may cause an update error.`,
+            vnode.context
+          )
+        } else {
+          seenKeys[key] = true
+        }
+      }
+    }
+  }
+
   function findIdxInOld (node, oldCh, start, end) {
     for (let i = start; i < end; i++) {
       const c = oldCh[i]

filename: test/unit/features/component/component.spec.js
patch: @@ -166,7 +166,7 @@ describe('Component', () => {
     const vm = new Vue({
       template:
         '<div>' +
-          '<component v-for="c in comps" :key="c.type" :is="c.type"></component>' +
+          '<component v-for="(c, i) in comps" :key="i" :is="c.type"></component>' +
         '</div>',
       data: {
         comps: [{ type: 'one' }, { type: 'two' }]

filename: test/unit/modules/vdom/patch/children.spec.js
patch: @@ -507,4 +507,27 @@ describe('vdom patch: children', () => {
 
     expect(postPatch).toBe(original)
   })
+
+  it('should warn with duplicate keys: createChildren', () => {
+    function makeNode (key) {
+      return new VNode('div', { key: key })
+    }
+
+    const vnode = new VNode('p', {}, ['b', 'a', 'c', 'b'].map(makeNode))
+    patch(null, vnode)
+    expect(`Duplicate keys detected: 'b'`).toHaveBeenWarned()
+  })
+
+  it('should warn with duplicate keys: updateChildren', () => {
+    function makeNode (key) {
+      return new VNode('div', { key: key })
+    }
+
+    const vnode2 = new VNode('p', {}, ['b', 'a', 'c', 'b'].map(makeNode))
+    const vnode3 = new VNode('p', {}, ['b', 'x', 'd', 'b'].map(makeNode))
+    patch(vnode0, vnode2)
+    expect(`Duplicate keys detected: 'b'`).toHaveBeenWarned()
+    patch(vnode2, vnode3)
+    expect(`Duplicate keys detected: 'b'`).toHaveBeenWarned()
+  })
 })