diff --git a/.changeset/gentle-doors-impress.md b/.changeset/gentle-doors-impress.md
new file mode 100644
index 000000000..d5b52236e
--- /dev/null
+++ b/.changeset/gentle-doors-impress.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+feat: add `svelte/require-each-key` rule
diff --git a/README.md b/README.md
index 0e6c8f6cf..3b78b7407 100644
--- a/README.md
+++ b/README.md
@@ -346,6 +346,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
| [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
| [svelte/prefer-destructured-store-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
+| [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | |
| [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | |
| [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | |
| [svelte/require-stores-init](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store | |
diff --git a/docs/rules.md b/docs/rules.md
index b8858ff59..8dd077cc1 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -59,6 +59,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
| [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
+| [svelte/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | |
| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | |
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |
| [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store | |
diff --git a/docs/rules/require-each-key.md b/docs/rules/require-each-key.md
new file mode 100644
index 000000000..0e15f8e40
--- /dev/null
+++ b/docs/rules/require-each-key.md
@@ -0,0 +1,51 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/require-each-key"
+description: "require keyed `{#each}` block"
+---
+
+# svelte/require-each-key
+
+> require keyed `{#each}` block
+
+- :exclamation: **_This rule has not been released yet._**
+
+## :book: Rule Details
+
+This rule reports `{#each}` block without key
+
+
+
+
+
+```svelte
+
+
+
+{#each things as thing (thing.id)}
+
+{/each}
+
+
+{#each things as thing}
+
+{/each}
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/keyed-each-blocks)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/require-each-key.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/require-each-key.ts)
diff --git a/src/rules/require-each-key.ts b/src/rules/require-each-key.ts
new file mode 100644
index 000000000..e721a0a26
--- /dev/null
+++ b/src/rules/require-each-key.ts
@@ -0,0 +1,27 @@
+import type { AST } from "svelte-eslint-parser"
+import { createRule } from "../utils"
+
+export default createRule("require-each-key", {
+ meta: {
+ docs: {
+ description: "require keyed `{#each}` block",
+ category: "Best Practices",
+ recommended: false,
+ },
+ schema: [],
+ messages: { expectedKey: "Each block should have a key" },
+ type: "suggestion",
+ },
+ create(context) {
+ return {
+ SvelteEachBlock(node: AST.SvelteEachBlock) {
+ if (node.key == null) {
+ context.report({
+ node,
+ messageId: "expectedKey",
+ })
+ }
+ },
+ }
+ },
+})
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index 7f02e373c..88965c3ca 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -45,6 +45,7 @@ import noUselessMustaches from "../rules/no-useless-mustaches"
import preferClassDirective from "../rules/prefer-class-directive"
import preferDestructuredStoreProps from "../rules/prefer-destructured-store-props"
import preferStyleDirective from "../rules/prefer-style-directive"
+import requireEachKey from "../rules/require-each-key"
import requireEventDispatcherTypes from "../rules/require-event-dispatcher-types"
import requireOptimizedStyleAttribute from "../rules/require-optimized-style-attribute"
import requireStoreCallbacksUseSetParam from "../rules/require-store-callbacks-use-set-param"
@@ -102,6 +103,7 @@ export const rules = [
preferClassDirective,
preferDestructuredStoreProps,
preferStyleDirective,
+ requireEachKey,
requireEventDispatcherTypes,
requireOptimizedStyleAttribute,
requireStoreCallbacksUseSetParam,
diff --git a/tests/fixtures/rules/require-each-key/invalid/each-block-without-key01-errors.yaml b/tests/fixtures/rules/require-each-key/invalid/each-block-without-key01-errors.yaml
new file mode 100644
index 000000000..10fc22cd9
--- /dev/null
+++ b/tests/fixtures/rules/require-each-key/invalid/each-block-without-key01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Each block should have a key
+ line: 19
+ column: 1
+ suggestions: null
diff --git a/tests/fixtures/rules/require-each-key/invalid/each-block-without-key01-input.svelte b/tests/fixtures/rules/require-each-key/invalid/each-block-without-key01-input.svelte
new file mode 100644
index 000000000..359b1937a
--- /dev/null
+++ b/tests/fixtures/rules/require-each-key/invalid/each-block-without-key01-input.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+{#each things as thing}
+
+{/each}
diff --git a/tests/fixtures/rules/require-each-key/valid/keyed-each-block01-input.svelte b/tests/fixtures/rules/require-each-key/valid/keyed-each-block01-input.svelte
new file mode 100644
index 000000000..840b949bc
--- /dev/null
+++ b/tests/fixtures/rules/require-each-key/valid/keyed-each-block01-input.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+{#each things as thing (thing.id)}
+
+{/each}
diff --git a/tests/src/rules/require-each-key.ts b/tests/src/rules/require-each-key.ts
new file mode 100644
index 000000000..e49b22c36
--- /dev/null
+++ b/tests/src/rules/require-each-key.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/require-each-key"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run("require-each-key", rule as any, loadTestCases("require-each-key"))