React Hooks vs. Vue 3 Composition API

Als React Hooks Ende 2018 eingeführt wurden, revolutionierten sie die Developer Experience, indem sie eine neue Art und Weise eingeführt haben, "React-ige" Logik zu teilen und wiederzuverwenden. Sogar der Schöpfer von Vue erkannte das und wollte es den Vue-Benutzern ermöglichen, die Funktionen dieses neuen Konzepts auf eine Weise zu nutzen, die die idomatische API von Vue ergänzt. Heute möchte ich das Ergebnis dieser Arbeit in Form der Vue 3 Composition-API mit React-Hooks vergleichen, indem ich eine Beispiel-App implementiere. Wir werden untersuchen, ob der auf "transparent reactivity" basierende Ansatz von Vue wirklich einfacher ist als der auf Immutability basierende Ansatz von React und welchen Nutzen das React-Team langfristig erzielen will, indem wir einen Blick auf den React Concurrent Mode werfen

Die Geschichte von Hooks

React Hooks wurden auf der React Conf im Oktober 2018 vorgestellt. (Wenn Du etwas Zeit hast und noch nicht die Keynote von Sophie Alpert, Dan Abramov und Ryan Florence gesehen hast, solltest Du sie dir unbedingt ansehen). Zunächst reagierte die Community mit "ein wenig" Skepsis, doch inzwischen sind Hooks und Funktionskomponenten für viele Teams zum Defacto-Standard bei der Entwicklung von React-Komponenten geworden.

Dieser Blog-Beitrag wird nicht direkt auf die Vorteile und Gründe für Hooks eingehen. Dazu kannst Du einen Blick auf den offiziellen Blog-Post zur Veröffentlichung werfen, der einige Ressourcen sammelt und weiter ins Detail bzgl. Motivation und Detailwissen geht.

Nichtsdestotrotz möchte ich den größten Mehrwert von React Hooks hervorheben: Die Wiederverwendbarkeit der "React-ful" Logik. Mit "React-ful" meine ich die Logik, die die Interna von React wie Komponentenzustand und Komponenten-Lebenszyklus verwendet. Hooks waren die erste API, die die Kapselung dieser Art von Logik in einem Modul ermöglichte, ohne Namenskonflikte oder undurchsichtige APIs einzuführen, bei denen sich die Entwickler fragen würden, woher eine Variable stammt (was bei früheren Ansätzen wie mixins oder Higher Order Components der Fall war).

Dieses Feature hat sogar Evan You, den Schöpfer von Vue, fasziniert. Auf Twitter bemerkte er, dass Hooks als Kompositionsmechanismus "objektiv besser" sind als frühere Ansätze wie Mixins, Higher Order Components oder Render-Props (Quelle). Er hatte jedoch ein paar Probleme mit der neu eingeführten API und machte sich auf die Suche nach etwas Besserem, das genau in die Welt von Vue passt. Das Ergebnis dieser Arbeit ist das neue Reaktivitätssystem von Vue 3 in Verbindung mit der neuen Composition API. Falls Du diese neue API schon kennst, aber noch nicht überzeugt bist, empfehle ich die offizielle RFC-Seite, die sehr detailliert über die Motivation hinter der Composition API berichtet.

In diesem Beitrag möchte ich diese beiden Ansätze vergleichen, um Unterschiede und Gemeinsamkeiten zu finden und Dir die Möglichkeit zu geben, etwas über Dein eigenes Setup zu lernen, unabhängig davon, ob Du Vue oder React verwendest.

Disclamer: Ich bin hauptsächlich React-Entwickler, aber ich bewundere die Einfachheit der Composition API. Ich werde versuchen, so unvoreingenommen wie möglich zu sein, aber am Ende habe ich immer noch viel mehr Erfahrung mit React-Hooks, was sich definitiv auf mein mentales Modell ausgewirkt hat, sodass einige Vue-Konzepte für mich unüblich sein könnten. Ich lasse mich sehr gerne von Dir korrigieren, wenn ich bei der Vue-Seite der Dinge etwas falsch mache 😊.

Stelle dein eigenes Pokémon-Team zusammen

Um die beiden Ansätze zu vergleichen, habe ich eine einfache App mit beiden Bibliotheken erstellt. Ein Pokémon-Team-Konfigurator. Sie besteht aus 2 einfachen Ansichten mit 3 relevanten Komponenten.

Auf der ersten Ansicht sieht man sein aktuelles Team, man kann Pokémon entfernen und über die Suchleiste unten neue Pokémon zu seinem Team hinzufügen. Wenn man auf den Namen eines seiner Teammitglieder klickt, gelangt man zu einer Detailansicht, auf der man den Spitznamen und einen Kommentar für das ausgewählte Pokémon bearbeiten kann. Die Daten für diese App werden in einem einfachen Backend gespeichert, das Endpunkte für das Abrufen des gesamten Teams, die Suche nach Pokémon nach Namen und die Handhabung der Details für jedes einzelne Pokémon bietet.

Einfacher Komponentenzustand

Zuerst wollen wir einen Blick auf die Handhabung von einfachem UI-State mit Hooks und der Composition-API werfen. Für diesen Fall definieren wir Zustand als ein Stück Daten, das sich im Laufe der Zeit ändert und von der UI widergespiegelt wird. Als Beispiel sehen wir uns zunächst die Suchkomponente an. Hier wollen wir ein Textfeld und senden Fetch-Requests ab, wenn sich der Inhalt dieses Feldes ändert. Für den Anfang werden wir die Serverkommunikation weglassen und uns auf die Synchronisierung zwischen Zustand und UI konzentrieren.

useState in React

import React, { useState } from "react";

export function Search() {
  const [searchValue, setSearchValue] = useState("");

  return (
    <input
      value={searchValue}
      onChange={(e) => setSearchValue(e.target.value)}
    />
  );
}

Hier sehen wir den einfachsten Anwendungsfall von Hooks: Wann immer wir Daten benötigen, die sich im Laufe des Lebenszyklus einer Komponente ändern, definieren wir den Zustand der Komponente, indem wir die useState Hook verwenden. Wir geben den Anfangswert dieses Zustands-Slots an und bekommen ein Tupel zurück, ein Array mit zwei Werten. Der erste ist der aktuelle Wert der Zustandsvariable, und der zweite ist die Updater-Funktion, die verwendet werden muss, um diesen Wert zu ändern.

Dies demonstriert das zugrundeliegende mentale Modal der Funktionskomponenten von React: Sie sind als Standard-JavaScript-Funktionen geschrieben, die immer dann aufgerufen werden, wenn sich "etwas" ändert. Wann immer sich also unser Zustand ändert (weil jemand setSearchValue aufgerufen hat z.B.), ruft React unsere Funktion erneut auf, wobei alle Variablen aus der vorherigen Ausführung gelöscht werden, während nur der Inhalt des Zustands der Komponente über die Aufrufe hinweg erhalten bleibt. Auf diese Weise ändern sich die Variablen in diesem Funktionsblock nie und können als const definiert werden, weil sie mit jedem Rerender neu deklariert werden.

Zusätzlich bedeutet dies, dass der Zustand selbst in React niemals mutiert wird. Er wird einfach durch den Aufruf des State-Updaters ersetzt, damit React dann den besten Zeitpunkt für das Rendern der Komponenteninstanz mit dem neuen Zustandswert bestimmen kann. Auf diese Weise bleibt unser eigener Code komplett zustandslos, was eine sehr wichtige Nuance ist, die benötigt wird, damit Reacts Concurrent Mode optimal funktioniert.

ref in Vue

<script>
import { ref } from "@vue/composition-api";

export default {
  setup(props) {
    const search = ref("");

    return { search };
  },
};
</script>

<template>
  <div>
    <input v-model="search" />
    <input :value="search" @input="search = $event.target.value" />
  </div>
</template>

Auf den ersten Blick sieht das ziemlich ähnlich aus wie die React-Version: Wann immer wir Daten benötigen, die sich während der Lebensdauer einer Komponenteninstanz ändern, können wir die ref Funktion aus der neuen Composition-API verwenden. Diese Funktion gibt ein Objekt mit einem Feld zurück: value. Und in diesem Feld hält Vue den aktuellen Wert des Zustands. Der Hauptvorteil dieses Ansatzes ist, dass Vue die Änderungen an diesem Feld verfolgen kann (durch die Verwendung von Proxy oder setter). Das bedeutet, dass Du als Entwickler keine spezielle Update-Funktion aufrufen musst. Du kannst diesem value-Feld einfach neue Werte zuweisen und sogar Objekte oder Arrays innerhalb dieses refs mutieren. Zusätzlich erlaubt dies Vue, die setup-Funktion nur einmal pro Komponenteninstanz aufzurufen. Dies richtet alle reaktiven Zustandsberechnungen ein und gibt die resultierenden Werte an das Template zurück.

Im Template-Teil hilft einem Vue und packt alle ref-Objekte aus, so dass man das nicht selbst tun muss, wie man in den Zeilen 15 und 16 sieht. Für einfache Fälle reicht es aus, das ref-Objekt über das v-model zu binden, aber hier möchte ich demonstrieren, was im Hintergrund passiert: Vue knüpft die value-Eigenschaft des inputs an den Inhalt des Search-Refs, sodass immer dann, wenn sich der Inhalt dieses Refs ändert (durch Zuweisung oder Mutation), die UI aktualisiert wird. Zusätzlich hängen wir einen Event Listener an das input-Event und weisen in diesem dem ref den neuen Wert des inputs zu. Dies löst das Reaktivitätssystem von Vue aus und führt ebenfalls zu einer aktualisierten UI.

Gefällt dir der Artikel? Folge mir auf Twitter, um keine neuen Inhalte zu verpassen.

Abgeleiteter Zustand

Als nächstes wollen wir einen Blick auf den abgeleiteten Zustand werfen: Werte, die anhand anderer aktueller Zustandswerte der Komponente berechnet werden können. Beispiel dafür ist die Validierung von Formularen (die nur von den aktuellen Werten des Formulars abhängt). Um es etwas einfacher zu machen, zeigen wir uns hier einfach die Länge des eingegebenen Textes an, mit der wir später entscheiden können, ob wir eine Suchanfrage senden wollen (weil wir vielleicht keine Anfragen mit 2 oder weniger Zeichen senden wollen).

Just JavaScript™ in React

import React, { useState } from "react";

export function Search() {
  const [searchValue, setSearchValue] = useState("");
  const length = searchValue.length;

  return (
    <>
      <input
        value={searchValue}
        onChange={(e) => setSearchValue(e.target.value)}
      />
      <span>Length: {length}</span>
    </>
  );
}

Da React unsere Funktion immer dann aufruft, wenn sich der Zustand ändert, können wir den Wert der searchValue-Konstante verwenden und die Länge direkt in der Render-Funktion ohne React-spezifische Logik berechnen. Auf diese Weise wird der berechnete Wert immer mit dem aktuellen Zustand synchron sein und wir müssen Zustände nicht manuell synchronisieren.

computed in Vue

<script>
import { ref, computed } from "@vue/composition-api";

export default {
  setup(props) {
    const search = ref("");
    const length = computed(() => search.value.length);

    return { search, length };
  },
};
</script>

<template>
  <div>
    <input v-model="search" />
    <input :value="search" @input="search = $event.target.value" />
    <span>Length: {{ length }}</span>
  </div>
</template>

In Vue hingegen müssen wir diese Aufgabe etwas anders angehen. Da die Setup-Funktion nur einmal pro Komponenteninstanz aufgerufen wird, können wir nicht einfach irgendeinen Zustand aus einer Variablen ableiten, da alle Zustandsvariablen innerhalb von veränderbaren ref-Objekten gekapselt sind. Unser Ziel ist es, auf Änderungen innerhalb des Search-Refs zu hören und Berechnungen durchzuführen, wenn sich das Ref ändert. Das ist genau das, was der computed helper tun soll: Wir übergeben eine Getter-Funktion, die den aktuellen Wert dieses neuen Refs berechnet. Darin können wir auf andere reaktive Objekte (z.B. props) zugreifen und Vue wird die Neuberechnung dieser Getter-Funktion übernehmen, wenn sich eine verwendete Abhängigkeit ändert. computed gibt uns anschließend ein read-only Ref zurück.

Man muss jedoch beachten, dass wir immer auf die Eigenschaft value der Refs zugreifen. Wie oben erwähnt, benötigt Vue diesen Wrapper um den Wert, damit es Abhängigkeiten wie den computed Längenwert in Zeile 7 tracken kann.

Hier können wir schön den Unterschied zwischen der veränderlichen Welt von Vue und der unveränderlichen Denkweise in React sehen: Bei Mutability muss man sicherstellen, dass kein Code die Quelldaten mutiert, ohne dass der eigene Code das bemerkt. Die Hilfsfunktionen von Vue erlauben dies auf sehr elegante Weise: Man muss selbst keine Abhängigkeiten manuell spezifizieren, man muss nur daran denken, Berechnungen immer in computed durchzuführen. Auf diese Weise baut man sich diese reaktiven Rechenketten auf. React hingegen führt alle Zustandsänderungen nur innerhalb der Bibliothek selbst durch, so dass der eigene Code sich nur noch um "Momentaufnahmen" in der Zeit kümmert. Die Funktionskomponenten nehmen diesen Snapshot, der alle Zustände enthält, und erzeugen die neue Version der Benutzeroberfläche einschließlich der abgeleiteten Zustände.

Eine wirklich coole Sache des Ansatzes von Vue, den ich hier hervorheben möchte: Man kann dieses Reaktivitätssystem verwenden, ohne Vue für die UI-Schicht zu verwenden. Helfer wie ref oder computed funktionieren außerhalb von Vue's Setup-Methode völlig problemlos. Man kann einfach ein veränderbares Objekt mit ref spezifizieren und Änderungen mit computed so verfolgen:

  import { ref, computed } from "@vue/composition-api";

  const changingCounter = ref(0);

  setInterval(() => {
    changingCounter.value++;
  }, 1000);

  const doubled = computed(() => changingCounter.value * 2);

  setInterval(() => {
    console.log(doubled.value);
  }, 500);

Am Ende nimmt Vue einfach diese ref-Objekte und fügt seine eigene UI-Generierungslogik als Abhängigkeit hinzu.

Bisher haben wir analysiert, wie man Zustände definieren, Werte im Laufe der Zeit ändern und Zustände sowohl in React als auch in Vue ableiten kann. Dies führt uns zu unserem nächsten Thema:

Side-Effects

In den meisten Fällen werden Anwendungen nicht nur lokale Daten verarbeiten, sondern auch mit einem Backend interagieren. Dazu müssen Komponenten auf lokale Zustandsänderungen reagieren, mit einer entfernten Instanz kommunizieren und die Ergebnisse zurück in das Zustandsverfolgungssystem der UI-Bibliothek einspeisen, damit die UI entsprechend aktualisiert werden kann.

useEffect in React

Wann immer man zwei Datenquellen (wie z.B. UI und Server) synchronisieren möchte, greift man in React zu useEffect:

import React, { useState, useEffect } from "react";
import { getInfo } from "../server/apiClient";

export function Search() {
  const [searchValue, setSearchValue] = useState("");
  const [serverResult, setServerResult] = useState(null);

  useEffect(() => {
    setServerResult(null);
    if (searchValue.length <= 2) return;
    getInfo(searchValue).then((result) => setServerResult(result));
  }, [searchValue]);

  return (
    <>
      <input
        value={searchValue}
        onChange={(e) => setSearchValue(e.target.value)}
      />
      {serverResult && <img src={serverResult.img} />}
    </>
  );
}

Zuerst definieren wir einen zweiten Zustand (wieder Daten, die sich im Laufe der Zeit ändern) für das Serverergebnis. Dann rufen wir useEffect mit zwei Argumenten auf: Das zweite ist ein Array von Werten, die beobachtet werden sollen, "dependencies" genannt, und das erste Argument ist eine Funktion, die immer dann ausgeführt wird, wenn sich mindestens eine der Abhängigkeiten zwischen den Rendervorgängen ändert. Da die Zustandsänderung nicht im eigenen Code geschieht, sondern nur innerhalb von React, müssen wir React manuell mitteilen, wann Effekte erneut ausgeführt werden sollen.

In diesem Fall "hören" wir auf Änderungen des searchValue-strings, und wenn diese Zeichenkette länger als 2 Zeichen ist, rufen wir die getInfo-Funktion auf und speichern das Ergebnis in unserer Zustandsvariable, um das Ergebnis wieder in React einzuspeisen. Diesen Zustand können wir dann in unserer UI verwenden. In diesem einfachen Fall ist es ganz offensichtlich, welche Variablen wir in das Abhängigkeitsarray einfügen müssen. Ich denke jedoch, dass man sehen kann, dass dies ziemlich schnell komplex werden könnte, da man leicht Abhängigkeiten vergessen könnte was zu veralteten Daten in den Abfragen führt. Für einen tieferen Einblick in useEffect empfehle ich dringend den Complete Guide to useEffect von Dan Abramov.

watchEffect in Vue

In Vue sieht dieser Code etwas einfacher aus:

<script>
import { ref, watchEffect } from "@vue/composition-api";
import { getInfo } from "../server/apiClient";

export default {
  setup(props) {
    const search = ref("");
    const serverResult = ref(null);

    watchEffect(() => {
      serverResult.value = null;
      if (search.value.length <= 2) return;
      getInfo(search.value).then((result) => {
        serverResult.value = result;
      });
    });

    return { search, serverResult };
  },
};
</script>

<template>
  <div>
    <input v-model="search" />
    <img v-if="serverResult" :src="serverResult.img" />
  </div>
</template>

Das erste, was vielleicht auffällt, ist, dass wir die Abhängigkeiten nicht manuell angeben müssen. Da das Reaktivitätssystem von Vue über eine eingebaute Abhängigkeitsverfolgung verfügt, kann sich die watchEffect-Funktion genau in diese Abhängigkeiten einklinken. Da wir auf den Wert von search zugegriffen haben, weiß Vue, dass diese Funktion erneut aufgerufen werden sollte, wann immer sich dieses Ref ändert. Wenn das von getInfo zurückgegebene Promise aufgelöst wird, schreiben wir das Ergebnis einfach in das serverResult ref, was das Reaktivitätssystem auslöst und zu aktualisierten berechneten Werten und UI führt.

An dieser Stelle muss man wirklich darauf achten, dass man bei der Arbeit mit Refs nicht vergisst, den .value. Property Lookup hinzuzufügen. Dies ist eine der Stellen, an denen es wirklich vorteilhaft ist, die verbesserte TypeScript-Unterstützung in Vue 3 zu verwenden, damit man solche Lookups nicht vergessen kann.

Und natürlich funktioniert watchEffect auch außerhalb von Vue-Komponenten:

  import { ref, computed, watchEffect } from "@vue/composition-api";

  const changingCounter = ref(0);

  setInterval(() => {
    changingCounter.value++;
  }, 1000);

  const doubled = computed(() => changingCounter.value * 2);

  // Triggered whenever doubled changes
  // which changes whenever the counter changes.
  watchEffect(() => console.log(doubled.value));

Wiederverwendbare Logik

Bis jetzt konnte alles, was wir gemacht haben, sehr leicht mit React-Klassen oder der traditionellen Vue Options API erreicht werden. Wo die neuen Zusätze wirklich glänzen, ist das Schreiben wiederverwendbarer zustandsabhängiger Logik. Dafür wollen wir eine einfache Custom Hook schreiben, die verwendet werden kann, um Datenabhängigkeiten auf deklarativere Weise zu spezifizieren, indem man die Hooks aus dem vorherigen Beispiel extrahiert:

Custom React Hook

import React, { useState, useEffect } from "react";
import { getInfo } from "../server/apiClient";

export function Search() {
  const [searchValue, setSearchValue] = useState("");

  const serverResult = useServerData(
    getInfo,
    searchValue.length > 2 ? searchValue : null
  );

  return (
    <>
      <input
        value={searchValue}
        onChange={(e) => setSearchValue(e.target.value)}
      />
      {serverResult && <img src={serverResult.img} />}
    </>
  );
}

function useServerData(asyncFunction, asyncFunctionArg) {
  const [serverResult, setServerResult] = useState(null);

  useEffect(() => {
    setServerResult(null);
    if (!asyncFunctionArg) return;

    asyncFunction(asyncFunctionArg).then((result) => setServerResult(result));
  }, [asyncFunctionArg, asyncFunction]);

  return serverResult;
}

Durch das Herausziehen der "useState" und "useEffect" Hooks haben wir eine wiederverwendbare Funktion zum Abrufen von Daten geschaffen. Diese kann eine sehr nützliche Abstraktion sein, die man dann an mehreren Stellen seiner Anwendung verwenden kann. Zusätzlich zum einfachen Abrufen von Daten könnte man Caching, Fehlerbehandlung, Abfrage-Deduplizierung und vieles mehr hinzufügen. (Alternativ könnte man auch einfach React Query verwenden.) Als Benutzer dieser Hook stellt man einfach eine Funktion bereit, die die Daten vom Server abruft, sowie ein Argument für diese Funktion, und überlässt der Hook die Orchestrierung der Abfragelogik (die in diesem Fall einfach gehalten ist).

Jedes Mal, wenn die Komponenteninstanz neu gerendert wird, rufen wir die useServerData-Hook mit der asynchronen Funktion und einem Argument für diese Funktion auf. Da wir beide Werte in das Abhängigkeitsarray von useEffects übergeben haben, wird der Effekt immer dann ausgelöst, wenn wir die Funktion oder das Argument ändern, und wir sollten niemals veraltete Daten erhalten. Auf diese Weise muss die Hook keine Annahmen darüber treffen, welche Argumente dieser Funktionen sich im Laufe der Zeit ändern könnten.

Custom Vue Composition Function

<script>
import { ref, watchEffect } from "@vue/composition-api";
import { getInfo } from "../server/apiClient";

export default {
  setup(props) {
    const search = ref("");

    const serverResult = useServerData(
      getInfo,
      computed(() => (search.value.length <= 2 ? null : search.value))
    );

    return { search, serverResult };
  },
};

function useServerData(asyncFunction, asyncFunctionArgRef) {
  const serverResult = ref(null);

  watchEffect(() => {
    serverResult.value = null;
    if (!asyncFunctionArgRef.value) return;
    asyncFunction(asyncFunctionArgRef.value).then((result) => {
      serverResult.value = result;
    });
  });

  return serverResult;
}
</script>

<template>
  <div>
    <input v-model="search" />
    <img v-if="serverResult" :src="serverResult.img" />
  </div>
</template>

Konzeptionell ist das im Grunde derselbe Ansatz. Der einzige Unterschied ist, dass in diesem Fall die useServerData Funktion nur einmal aufgerufen wird, da die setup Funktion der Komponente nur einmal pro Instanz aufgerufen wird. Unsere Kompositionsfunktion muss in der Lage sein, mit Argumenten zu arbeiten, die sich im Laufe der Zeit ändern. In diesem Beispiel haben wir angenommen, dass sich die asyncFunction im Laufe der Zeit nicht ändern muss, während sich das Argument ändert. Deshalb haben wir dieses argRef genannt. In TypeScript hätten wir die Signatur so geschrieben:

function useServerData<ResultType, ArgType>(
  fn: (arg: ArgType) => Promise<ResultType>,
  arg: Ref<ArgType | null>
): Ref<ResultType | null> {
  // ...
}

Diese Funktion empfängt also das Argument als ref und gibt ein ref zurück, welches das Ergebnis des aktuell durchgeführten Serverrequests enthält.

Auch hier erlaubt uns das Extrahieren dieser Logik, später weitere Funktionen wie Caching, bessere Fehlerbehandlung oder Authentifizierung hinzuzufügen, ohne anderen Code in unserer Anwendung anfassen zu müssen.

Zusammenfassung: Mutierbare, implizite Reaktivität vs. unveränderliche explizite Änderungen

Zusammenfassend lässt sich sagen, dass diese beiden APIs es uns Entwicklern erlauben, Abstraktionen zu erstellen, die es uns ermöglichen, zustandsbehaftete Logik in kleinere Kompositionseinheiten zu kapseln: Custom Hooks in React und Custom Composition Functions in Vue. Der Hauptunterschied zwischen den Ansätzen besteht darin, wie sie mit Zustandsänderungen umgehen: Während Vue es uns erlaubt, Daten einfach zu mutieren, indem wir sie in ein ref packen, zwingt uns React dazu, die Daten unveränderlich zu halten und sie durch explizite State-Updater zu aktualisieren. Auf der Vue-Seite der Dinge kann die Bibliothek dann alle verwendeten Abhängigkeiten tracken und den abgeleiteten Zustand und die UI in Abhängigkeit von der Änderung eines Zustandswertes effizient aktualisieren. Auf der anderen Seite ruft React einfach alle Hooks und daraus resultierenden Berechnungen (die man mit useMemo zwischenspeichern könnte) wieder auf und leistet damit etwas mehr Arbeit, um die Verwendung von Standard-JavaScript-Werten zu ermöglichen, die nicht in irgendwelche Container verpackt sind.

Dies ist ein Punkt, der für mich noch nicht ganz klar ist: Wenn man mit veränderbaren Daten arbeitet, müssen alle Bibliotheken und jedes Modul, das mit diesen Daten arbeitet, damit zurecht kommen, dass die Daten im Laufe der Zeit verändert werden. Wenn man alle Teile der Anwendung kontrolliert und das Reaktivitätssystem von Vue überall einsetzen kann, wird das nie ein Problem sein, allerdings könnte die Integration mit Code von Drittanbietern eine kleine Hürde darstellen. Es würde mich sehr interessieren zu hören, ob einige von Euch jemals derart Probleme in Euren Anwendungen hatten. Fairerweise kann man das Gleiche in die entgegengesetzte Richtung sagen: Wenn man in einer unveränderlichen Welt arbeitet, können Mutationen den Code kaputt machen. Ein Beispiel, das mir recht häufig begegnet ist, ist die Moment.js Datums-Bibliothek. Sie mutiert standardmäßig Datumsobjekte, was die Dpendency-Arrays in React-Anwendungen zum Stolpern bringen kann.

Wenn also der Vue-Ansatz einfacher ist, indem er direkte Mutationen erlaubt, weniger fehleranfällig, indem er Abhängigkeiten automatisch verfolgt, und performanter, indem er Abhängigkeiten berechnet, müssen wir uns fragen: Warum verfolgt React diesen Ansatz? Die Antwort auf diese Frage ist im Grunde eine Wette auf die Zukunft und liegt in React's kommendem Concurrent Mode. Schauen wir uns mal das folgende Gif an:

Sobald man auf zapdos klickt, wird der aktuelle Bildschirm sofort durch eine leere Seite mit einer Ladeanzeige ersetzt. Da wir den Status (selectedMember) in einem ref direkt mutiert haben, aktualisiert sich die UI sofort und lädt den neuen Bildschirm, der dann auf Daten warten muss. Natürlich könnten wir die Ladeanzeige verbessern oder ein Skelett der nächsten Seite anzeigen, aber wir ersetzten trotzdem einen Bildschirm, der während der Ladezeit verwendet werden könnte (z.B. für das Ändern des ausgewählten Pokémon, während das erste geladen wird), durch einen, der dem Benutzer nur einige Ladebalken anzeigt. Keine gute User Experience.

Vergleichen wir das mit dem folgenden Gif:

Wir sehen, dass die alte UI so lange bestehen bleibt, bis alle Daten fertig geladen sind. Und erst dann schaltet die Benutzeroberfläche sofort auf den nächsten Bildschirm um, ohne Ladeanzeigen oder springende Bilder. Dies wird durch die Verwendung des Concurrent Mode ermöglicht:

Nachdem wir auf zapdos geklickt haben, sagen wir React: Bitte versuche, die App mit diesem gegebenen neuen Zustand zu rendern, und wenn irgendwelche Daten fehlen, warte einfach, bis sie geladen sind. React nimmt dann diesen neuen Zustandswert und versucht, die Anwendung mit diesem neuen Zustand im Speicher zu rendern (ohne dem Benutzer etwas zu zeigen). Es tut dies, bis es auf eine Datenabhängigkeit stößt (unter Verwendung von React Suspense) und wartet, bis diese Abhängigkeit aufgelöst ist. Während dieser Wartezeit wird die Anwendung nicht eingefroren, sondern bleibt voll funktionsfähig: Wir könnten einen Inline-Ladeindikator anzeigen, den Benutzer auf andere Elemente klicken lassen usw. Dies ist nur möglich, weil Zustandswerte niemals durch unseren Code mutiert werden, sondern nur innerhalb von React ersetzt werden. Auf diese Weise kann React gleichzeitig unsere zustandslosen Funktionen mit verschiedenen Versionen des Zustands aufrufen, ohne Fehler zu verursachen. Darüber hinaus funktioniert dies nicht nur für Wartezeiten aufgrund von Serveranfragen: In Fällen, in denen ein hoch-priorisiertes Zustands-Update (z.B. durch Eingabe des Benutzers) während der Vorbereitung des nächsten Bildschirms stattfindet, können wir zuerst das hoch-priorisierte Update zwischenschieben und dann mit dem teuren Update niedriger Priorität weitermachen.

Wie ich bereits sagte, ist dies im Grunde eine Wette auf die Zukunft: Concurrent Mode und Suspense for Data Fetching ist noch sehr experimentell. Wir können damit herumspielen, indem wir den experimentellen Zweig von React verwenden, aber das Ökosystem ist noch nicht ganz so weit. Das Versprechen von Bibliotheken, die Datenabhängigkeiten direkt neben unseren Komponentendefinitionen kapseln ist für mich jedoch sehr interessant und macht mich sehr neugierig auf die Zukunft von Frontend-Bibliotheken.

Ich hoffe, Du konntest etwas über die Ansätze und die Philosophien von React und Vue lernen. Wenn Du dich für den Quellcode der React- und Vue-Anwendungen interessierst, kannst Du einen Blick auf das GitHub-Repository werfen.