Christoph Haag

Christoph Haag

Christoph ist Softwareentwickler mit einer Passion für Musik

30.04.2024 | 4 min Lesezeit

Ich habe mein Array readonly gemacht. Du wirst nicht glauben, was dann passiert ist.

Ich habe mein Array readonly gemacht. Du wirst nicht glauben, was dann passiert ist. blog image

Was ist der Unterschied zwischen folgenden beiden Code-Schnipseln?

Variante 1
type State = {
  itemsA: ItemA[]
  itemsB: ItemB[]
}

Variante 2
type State = {
  itemsA: readonly ItemA[]
  itemsB: readonly ItemB[]
}

Der Unterschied ist, mit Variante 2 hätte ich mir mehrere Stunden Debugging und Kopfzerbrechen gespart.

React basiert im Wesentlichen auf der Gleichung f(state) = UI. Die Ausgabe ist nur von den Eingabeparametern abhängig. Das ist das Grundprinzip der funktionalen Programmierung und man nennt eine solche Funktion pure - nebeneffektfrei. Das bedeutet im Umkehrschluss auch, dass eine Funktion die Eingabeparameter nicht verändern darf.

Schauen wir uns den Code an, der das Problem verursacht hat. Es ist ein React hook zu sehen, der über einen Store auf zwei Listen itemsA und itemsB zugreift. Das Ergebnis enthält alle Elemente aus der A-Liste und soll zusätzlich alle Items aus der B-Liste enthalten, die nicht schon in der ersten Liste existieren (!result.some(x => isEqual(x, itemB))). Am Ende wird das Ergebnis noch sortiert und zurückgegeben.

useItems hook
const useItems = () => {
  const itemsA = useStore(x => itemsA)
  const itemsB = useStore(x => itemsB)

  const result: Item[] = itemsA

  for (itemB of itemsB) {
    if (!result.some(x => isEqual(x, itemB))) {
      result.push(itemB)
    }
  }

  return result.sort(sortFunction)
}

Sieht eigentlich gut aus, oder? Es gab sogar Unit-Tests, die diese Funktion abgetestet haben. 100% Code-Coverage. Alles im Grünen. TypeScript? Keine Fehler. Zur Laufzeit: Eine Katastrophe. Es gab Stellen in der UI, an denen nur Items aus der A-Liste angezeigt werden durften, aber es wurden auch Items aus der B-Liste angezeigt.

Es gab im gesamten Code nur eine einzige Methode, diese Items zu setzen und zu verändern. Ich habe gedebuggt, Unit-Tests für diese Methode geschrieben und bin verzweifelt. Diese Methode wurde immer korrekt aufgerufen. 100% Code-Coverage, keine TypeScript Fehler. Meine Liste an Items muss also noch an anderer Stelle verändert werden. Es muss eine Funktion geben, die auf die A-Liste zugreift und diese verändert. Es muss eine Funktion geben, die nicht pure ist.

Nach langem Suchen habe ich schließlich den oben gezeigten Code gefunden, untersucht und festgestellt: die useItems-Funktion ist das Problem. Eine einzige Zeile, ein flüchtiger Fehler, nicht offensichtlich, aber mit großer Tragweite.

problematischer Code
const result: Item[] = itemsA
// ...
result.push(itemB)

Die A-Liste wird verändert. Schwierig zu sehen, da itemsA zuerst der Variablen result zugewiesen wird. Die Methode push mutiert das Array. Mein Store, meine Datenstruktur, meine Unit-Tests, mein TypeScript-Compiler. Nichts hat mich davor gewarnt - alles umsonst.

Die Lösung in der Datenstruktur:

Angepasste Datenstruktur
type State = {
  itemsA: readonly ItemA[]
  itemsB: readonly ItemB[]
}

Meine Liste aus Items darf nicht verändert werden. Und siehe da, mit dieser Änderung rettet mich TypeScript.

TypeScript to the rescue
const useItems = () => {
  const itemsA = useStore(x => itemsA)
  const itemsB = useStore(x => itemsB)

  // The type 'readonly ItemA[]' is 'readonly' and cannot be assigned to the mutable type 'Item[]'.ts(4104)
  const result: Item[] = itemsA

  for (itemB of itemsB) {
    if (!result.some(x => isEqual(x, itemB))) {
      result.push(itemB)
    }
  }

  return result.sort(sortFunction)
}

TypeScript ist hier besonders charmant, da damit auch sichergestellt ist, dass der selbe Fehler beim Zugriff auf die Items-Liste nicht noch einmal passiert.

Die Lösung im Code:

Die Lösung
const result: Item[] = [...itemsA]
// ...
result.push(itemB)

Mit dieser Änderung wird ein neues Array erstellt und das B-Item hinzugefügt. Die ursprüngliche Liste bleibt unverändert. Die Funktion ist pure.

Problem solved. Time wasted. Lesson learned.

Weiter lesen