The Art of State: Zustandsmanagement in React-Anwendung, Teil 3

Seite 4: New Kid on the Block: Recoil

Inhaltsverzeichnis

Eine recht neue Bibliothek zum Zustandsmanagement ist Recoil. Sie wurde zwar erst Mitte 2020 veröffentlicht und ist somit erst ein knappes halbes Jahr alt. Recoil zog aber schnell viel Aufmerksamkeit auf sich, weil die Bibliothek – wie React – von Facebook stammt. Allerdings ist es keine "offizielle" React-Entwicklung. Sie wird von einem anderen Team betrieben und ist erst recht kein Bestandteil von React (was nicht heißen soll, dass nicht Teile davon in React irgendwann landen). Die Verbreitung von Recoil dürfte dementsprechend (noch) nicht an MobX oder gar Redux heranreichen.

Im Gegensatz zu MobX und Redux ist Recoil eine React-spezifische Entwicklung, die auch neuere React-Features wie Suspense und Concurrent Mode von Haus aus unterstützt. Die Daten hält Recoil in sogenannten Atomen (Atoms). Diese liegen außerhalb der React-Komponenten und sind somit, wie der Store in MobX oder Redux, nicht an den Lebenszyklus einer Komponente gebunden. Komponenten, die ein oder mehrere Atome verwenden, können die Daten verändern und werden automatisch benachrichtigt, sobald sich ein eingesetztes Atom verändert – ähnlich wie in einer Observer-Komponente in MobX. Die Entscheidung, ob eine Komponente neu gerendert wird oder nicht, ist dabei allerdings grobgranularer als in MobX: Während dieses eine Komponente nur erneut rendert, wenn sich die tatsächlich in der Komponente genutzten Daten ändern, rendert Recoil eine Komponente auch dann neu, wenn sich Teile eines Atoms verändert haben, die die Komponente gar nicht benötigt. Ob das in einer Anwendung von Relevanz ist, hängt unter anderem am Schnitt der Stores beziehungsweise Atome.

Ein Atom wird mit der atom-Funktion auĂźerhalb einer Komponente erzeugt. Es besteht aus einem Bezeichner (key) und einem initialen Wert (default):

const itemsState = atom({
  key: 'shoppingListItems',
  default: [
    { id: uuid(), 
      name: "Salad", 
      shop: "Organic", 
      done: false
    }
  ],
});

const orderByState = atom({
  key: 'orderBy',
  default: "name"
});

Auf ein Atom lässt sich in Komponenten aus beliebigen Stellen der Anwendung zugreifen, die den useRecoilState-Hook verwenden. Genau wie der useState-Hook liefert dieser Hook den aktuellen Wert eines Atoms und eine Setter-Funktion zum Verändern des Werts zurück. Das Verändern des Werts führt dazu, dass alle Komponenten, die das Atom nutzen, neu gerendert werden.

function OrderBy() {
  const [orderBy, setOrderBy] = useRecoilState(orderByState);

  return <div>Currently ordered by {orderBy}.
    <button onClick={ () => setOrderBy("name") }>Name</button>
    <button onClick={ () => setOrderBy("shop") }>Shop</button>
  </div>
}

Genau wie in React wird der in Atomen gehaltene Zustand als unveränderlich (immutable) angesehen. Ein Array oder Objekt im Zustand darf also nicht direkt verändert werden, sondern ist jeweils durch neuen zu ersetzen. Dieses Verhalten entspricht dem Verhalten von useState.

Ähnlich zu den Computed Properties in MobX lassen sich in Recoil Daten abhängig von anderen Daten berechnen. Dazu nutzt Recoil Selektoren (Selectors). Sie haben eine oder mehrere Abhängigkeiten auf Atome oder andere Selektoren. Wenn sich eine der Abhängigkeiten ändert, wird der Wert des Selektors neu berechnet. Genau wie die Atome werden auch die Selektoren außerhalb der React-Komponenten erzeugt. Hierfür kommt die Funktion selector zum Einsatz, der man eine Selektorbeschreibung übergibt. Sie besteht aus einer Bezeichnung (key) und einer Getter-Funktion (get), die den berechneten Wert zurückliefert.

Die get-Funktion selbst erhält als Parameter eine get-Callback-Funktion übergeben, mit der sich die benötigten Atome auswählen lassen. Ändert sich einer der ausgewählten Atome, wird die get-Funktion des Selektors erneut ausgeführt.

const orderedItemsSelector = selector({
  key: 'orderedItems',
  get: ({get}) => {
    const allItems = get(itemsState);
    const orderBy = get(orderByState);
   
    return ... /* allItems basierend auf orderBy sortieren */
  }
});

Selektoren können man in einer Komponente mit dem Hook useRecoilValue abfragen. Er lässt sich sowohl für Atome verwenden, auf die eine Komponente nur lesenden Zugriff benötigt, als auch für Selektoren.

function ShoppingList() {
  const orderedItems = useRecoilValue(orderedItemsSelector);

  return <div>
   {orderedItems.map(item => 
     <ShoppingItemView key={item.id} item={item} />
   )}
  </div>
}

Immer wenn sich das Ergebnis des Selektors ändert, wird die Komponente neu gerendert.