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
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
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.