Skip to content

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 functionEffectSeverity
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 remountsWarning
calls Array.IndexOf, ResizeArray.IndexOf, IList.IndexOf, Array.findIndex, List.findIndex or Seq.findIndexpositional key: a move or insert remounts the rows after itWarning
contains a reactive readthe key depends on a tracked value, of the item or outside itWarning
forEach with id or fun x -> x and a record or union item typea replaced item gets a new key, so the row remounts instead of following the itemInfo
key type compared by reference (a class without custom equality)rebuilt items get new keysInfo

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.

Edit this page