# RAN021 Unstable list key

`warning RAN021`: *This key is the item's position (`Array.IndexOf`), so moving or inserting an item remounts every row
after it.*

**Cause.** `forEach`, `forEachFixed` and `each` keep a row for an item whose key is equal under `=`, so a key function
must be pure. The rule reads the key lambda, or a function of the same file passed by name, with the same-file
functions it calls, and reports:

| Key function | Effect | Severity |
|---|---|---|
| writes a captured `mutable` or `ref`, or calls `Guid.NewGuid`, `Random`, `DateTime.Now`/`UtcNow`, `Environment.TickCount`, `Stopwatch` or `obj ()` | every pass gives new keys: every row remounts | Warning |
| calls `Array.IndexOf`, `ResizeArray.IndexOf`, `IList.IndexOf`, `Array.findIndex`, `List.findIndex` or `Seq.findIndex` | positional key: a move or insert remounts the rows after it | Warning |
| contains a reactive read | the key depends on a tracked value, of the item or outside it | Warning |
| `forEach` with `id` or `fun x -> x` and a record or union item type | a replaced item gets a new key, so the row remounts instead of following the item | Info |
| key type compared by reference (a class without custom equality) | rebuilt items get new keys | Info |

The `id` row applies to record and union items. A list of strings or primitives keyed by `id` is correct.

**Fix.** Key by a value unique to the item, such as an id field.

### Triggers

```fsharp
module Example

open System
open Ranvier
open Ranvier.View
open Ranvier.View.Sugar

type Todo = { Id: int; Label: string }

let todos = createSignal [| { Id = 1; Label = "a" } |]
let row = li () { fun (row: unit -> Todo) -> (row ()).Label }

let list =
    Component.template (fun _ () -> todos) (
        ul () { forEach (fun (todo: Todo) -> Array.IndexOf (todos.Peek, todo)) (fun (s: Signal<Todo array>) -> s.Value) row }
    )
```

### Fixed

```fsharp
module Example

open System
open Ranvier
open Ranvier.View
open Ranvier.View.Sugar

type Todo = { Id: int; Label: string }

let todos = createSignal [| { Id = 1; Label = "a" } |]
let row = li () { fun (row: unit -> Todo) -> (row ()).Label }

let list =
    Component.template (fun _ () -> todos) (
        ul () { forEach (fun (todo: Todo) -> todo.Id) (fun (s: Signal<Todo array>) -> s.Value) row }
    )
```

**Suppression.** `// fsharpanalyzer: ignore-line RAN021`, with the reason on the line before.
