Repository: sveltejs/svelte (JavaScript)
Date: 2020-01-22 15:56:23+00:00
Task type: Feature

Issue:
Throw dev mode exception when a keyed each has duplicate keys
**Is your feature request related to a problem? Please describe.**
Duplicate keys in a keyed each can cause confusing runtime errors.

**Describe the solution you'd like**
A dev-mode-only check for 

**Describe alternatives you've considered**
Making keyed eaches behave better when there are duplicate keys sounds like a lot of work and probably not a good idea anyway.

The only alternative I've really considered (besides doing nothing) is to run this check all the time with keyed eaches, not just in dev mode. But this would be a) more code for people whose apps are already working fine, and b) more work for the browser to do. I think this makes the most sense as a dev mode error (or _possibly_ a dev mode warning).

**How important is this feature to you?**
It'd be nice. It shouldn't be too hard (I have it basically working locally), and it doesn't introduce any extra cost to people in prod mode.

**Additional context**
See #4244.


Issue discussion:
We use `update_keyed_each` helper function to update a [keyed each logic blocks](https://svelte.dev/tutorial/keyed-each-blocks) in Svelte.

The implementation of `update_keyed_each` can be [seen here](https://github.com/sveltejs/svelte/blob/cd21acf/src/runtime/internal/keyed_each.ts#L24).

To add a dev-mode-only check for the `update_keyed_each`, you need to:

1. Implement `update_keyed_each_dev`. (the name is important, you need to suffix the runtime helper function with exactly `_dev` to be picked up by the Svelte compiler.)
1. You can add the `update_keyed_each_dev` in the [src/runtime/internal/dev.ts](https://github.com/sveltejs/svelte/blob/646c3df3c670e4d31db218aa0927576e1575beac/src/runtime/internal/dev.ts) file. You can read other dev-mode-only runtime helper function implemented there too!
1. for unit testing, it can refer to [this test case](https://github.com/sveltejs/svelte/tree/f45e2b70fdaad54e86fbdf725ed19176b8746262/test/runtime/samples/dev-warning-destroy-twice).
   - set the `compileOptions.dev = true`, and make sure it warns appropriately.
1. PR 🎉

---

If it is the first time that you contribute to Svelte, follow these steps:
1. **Write a comment here** to let other possible contributors to know that you are working on this issue
1. Fork the repo
1. Run `git clone https://github.com/<YOUR_USERNAME>/svelte.git && cd svelte`
1. Run `npm install`
1. Wait ⏳
1. Run `npm run dev` (or `npm run build` whenever you change a file)
1. Add a test
1. Update the code!
1. `npm run quicktest` to run the test without rebuilding
1. Run `git commit` with a meaningful commit message
1. `git push` and open a PR 🎉

You can read our [Contributing Guide](https://github.com/sveltejs/svelte/tree/master/CONTRIBUTING.md) to learn more.
It turned out that `update_keyed_each` wasn't the only place this needs to be checked, as that isn't called when the each block is initially created. I have a WIP on the branch [here](https://github.com/Conduitry/svelte/tree/gh-4301) that mostly just needs a little tidying then is probably ready for a PR. I ended up actually not using `update_keyed_each_dev` in my branch at all. I guess I don't really have a strong opinion about the tidiest way to handle this.
Saw your branch, looks good to me
Did you mean to remove the `measure` function from keyed_each?
https://github.com/Conduitry/svelte/commit/d36370c5994ae5d9d7d96081bd4eda31032e5f1c?diff=unified#diff-b66813450a6494de578b19bf54ea4083
It's an unrelated change, but I did intend to remove it. It's not used anywhere (I'm not sure when it last was), and it seems unrelated to the other keyed each stuff in the file.
Also see #4234, which has the same root cause.

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 * Fix updating a `<slot>` inside an `{#if}` or other block ([#4292](https://github.com/sveltejs/svelte/issues/4292))
 * Fix using RxJS observables in `derived` stores ([#4298](https://github.com/sveltejs/svelte/issues/4298))
+* Add dev mode check to disallow duplicate keys in a keyed `{#each}` ([#4301](https://github.com/sveltejs/svelte/issues/4301))
 
 ## 3.17.2
 
diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts
--- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts
+++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts
@@ -374,6 +374,7 @@ export default class EachBlockWrapper extends Wrapper {
 		block.chunks.init.push(b`
 			const ${get_key} = #ctx => ${this.node.key.manipulate(block)};
 
+			${this.renderer.options.dev && b`@validate_each_keys(#ctx, ${this.vars.each_block_value}, ${this.vars.get_each_context}, ${get_key});`}
 			for (let #i = 0; #i < ${data_length}; #i += 1) {
 				let child_ctx = ${this.vars.get_each_context}(#ctx, ${this.vars.each_block_value}, #i);
 				let key = ${get_key}(child_ctx);
@@ -416,6 +417,7 @@ export default class EachBlockWrapper extends Wrapper {
 
 			${this.block.has_outros && b`@group_outros();`}
 			${this.node.has_animation && b`for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].r();`}
+			${this.renderer.options.dev && b`@validate_each_keys(#ctx, ${this.vars.each_block_value}, ${this.vars.get_each_context}, ${get_key});`}
 			${iterations} = @update_keyed_each(${iterations}, #dirty, ${get_key}, ${dynamic ? 1 : 0}, #ctx, ${this.vars.each_block_value}, ${lookup}, ${update_mount_node}, ${destroy}, ${create_each_block}, ${update_anchor_node}, ${this.vars.get_each_context});
 			${this.node.has_animation && b`for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].a();`}
 			${this.block.has_outros && b`@check_outros();`}
diff --git a/src/runtime/internal/keyed_each.ts b/src/runtime/internal/keyed_each.ts
--- a/src/runtime/internal/keyed_each.ts
+++ b/src/runtime/internal/keyed_each.ts
@@ -108,9 +108,13 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list
 	return new_blocks;
 }
 
-export function measure(blocks) {
-	const rects = {};
-	let i = blocks.length;
-	while (i--) rects[blocks[i].key] = blocks[i].node.getBoundingClientRect();
-	return rects;
+export function validate_each_keys(ctx, list, get_context, get_key) {
+	const keys = new Set();
+	for (let i = 0; i < list.length; i++) {
+		const key = get_key(get_context(ctx, list, i));
+		if (keys.has(key)) {
+			throw new Error(`Cannot have duplicate keys in a keyed each`);
+		}
+		keys.add(key);
+	}
 }


Test patch:
diff --git a/test/runtime/samples/keyed-each-dev-unique/_config.js b/test/runtime/samples/keyed-each-dev-unique/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/keyed-each-dev-unique/_config.js
@@ -0,0 +1,7 @@
+export default {
+	compileOptions: {
+		dev: true
+	},
+
+	error: `Cannot have duplicate keys in a keyed each`
+};
diff --git a/test/runtime/samples/keyed-each-dev-unique/main.svelte b/test/runtime/samples/keyed-each-dev-unique/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/keyed-each-dev-unique/main.svelte
@@ -0,0 +1,7 @@
+<script>
+	const array = [1, 2, 3, 1];
+</script>
+
+{#each array as item (item)}
+	{item}
+{/each}