diff --git a/.changeset/volatile-each-key.md b/.changeset/volatile-each-key.md new file mode 100644 index 0000000000..674bce9bec --- /dev/null +++ b/.changeset/volatile-each-key.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: detect and error on non-idempotent each block keys in dev mode diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 8601a728a7..7fccac5808 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -62,6 +62,14 @@ Keyed each block has duplicate key at indexes %a% and %b% Keyed each block has duplicate key `%value%` at indexes %a% and %b% ``` +### each_key_volatile + +``` +Keyed each block has key that is not idempotent — the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item +``` + +The key expression in a keyed each block must return the same value when called multiple times for the same item. Using expressions like `[item.a, item.b]` creates a new array each time, which will never be equal to itself. Instead, use a primitive value or create a stable key like `item.a + '-' + item.b`. + ### effect_in_teardown ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index bedf6db0a5..3f20cb989d 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -42,6 +42,12 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long > Keyed each block has duplicate key `%value%` at indexes %a% and %b% +## each_key_volatile + +> Keyed each block has key that is not idempotent — the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item + +The key expression in a keyed each block must return the same value when called multiple times for the same item. Using expressions like `[item.a, item.b]` creates a new array each time, which will never be equal to itself. Instead, use a primitive value or create a stable key like `item.a + '-' + item.b`. + ## effect_in_teardown > `%rune%` cannot be used inside an effect cleanup function diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 25f7cf91eb..7ae02d073c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -250,6 +250,14 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var value = array[index]; var key = get_key(value, index); + if (DEV) { + // Check that the key function is idempotent (returns the same value when called twice) + var key_again = get_key(value, index); + if (key !== key_again) { + e.each_key_volatile(String(index), String(key), String(key_again)); + } + } + var item = first_run ? null : items.get(key); if (item) { diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 34f1d85540..d60c2dd280 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -147,6 +147,25 @@ export function each_key_duplicate(a, b, value) { } } +/** + * Keyed each block has key that is not idempotent — the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item + * @param {string} index + * @param {string} a + * @param {string} b + * @returns {never} + */ +export function each_key_volatile(index, a, b) { + if (DEV) { + const error = new Error(`each_key_volatile\nKeyed each block has key that is not idempotent — the key for item at index ${index} was \`${a}\` but is now \`${b}\`. Keys must be the same each time for a given item\nhttps://svelte.dev/e/each_key_volatile`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/each_key_volatile`); + } +} + /** * `%rune%` cannot be used inside an effect cleanup function * @param {string} rune diff --git a/packages/svelte/tests/runtime-runes/samples/each-key-volatile/_config.js b/packages/svelte/tests/runtime-runes/samples/each-key-volatile/_config.js new file mode 100644 index 0000000000..87d4bf45c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-key-volatile/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + mode: ['client'], + + error: 'each_key_volatile' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-key-volatile/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-key-volatile/main.svelte new file mode 100644 index 0000000000..689c257cfa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-key-volatile/main.svelte @@ -0,0 +1,10 @@ + + +{#each things as thing ([thing.group, thing.id])} +

{thing.group}-{thing.id}

+{/each}