GraphQL-Clients mit React und Apollo, Teil 2

Alternativen zu komponentenbasierten Abfragen

Die deklarative Programmierung mit Query- und Mutation-Komponenten entspricht zwar der React-Philosophie, kann aber in der Benutzung gewöhnungsbedürftig sein. Außerdem können die Komponentenhierarchien unübersichtlich sein, beispielsweise wenn eine Komponente mehrere Queries absetzen möchte. Als Alternative dazu kann man diegraphql-Higher-Order-Component (HOC) verwenden, mit der Nutzer Queries und Mutations samt ihrer Konfiguration aus der fachlichen Komponente herausziehen können, sodass sie sich auf die reine Darstellung der geladenen Daten konzentrieren können.

Eine weitere Alternative ist die withApollo HOC, die ein ApolloClient-Objekt in die ummantelte Komponente übergibt. Über dieses hat die Komponente Zugriff auf query- und mutation-Funktionen, die man nutzen kann, um GraphQL-Abfragen programmatisch auszuführen. Die oben geschilderten Features etwa hinsichtlich des Cachings bleiben weiterhin gültig, sodass alle gezeigten Ansätze innerhalb einer Anwendung beliebig kombiniert und gemeinsam genutzt werden können.

Typsichere Queries mit TypeScript

Eine GraphQL API ist immer mit einem Schema hinterlegt, aus dem hervorgeht, welche Queries und Mutations zulässig sind, welche Objekte die Operationen zurückgeben, und wie sie selber strukturiert sind. Das Schema einer API können Anwender mit einer standarisierten Query von der Schnittstelle abfragen. Aufbauend auf dem Feature gibt es die Möglichkeit, durch den Einsatz von TypeScript typsicher mit GraphQL-Schnittstellen zu arbeiten. Das bedeutet, dass zur Entwicklungszeit sichergestellt ist, dass eine Query mit den korrekten Variablen aufgerufen wird und deren Ergebnis eine gültige Verwendung findet (also zum Beispiel nicht auf Felder zugreift, die gar nicht abgefragt wurden).

Um es mit Typescript zu nutzen, müssen Entwickler zunächst Typen für die Queries beziehungsweise deren Variablen und Ergebnisse definieren. Die Typen kann man von Hand schreiben oder mit dem Apollo-Code-Generator automatisch erzeugen. Der Code-Generator liest zunächst das aktuelle Schema der API ein und generiert darauf aufbauend für alle Queries und Mutations der Anwendung die passenden Typen. Sie bestehen in der Regel aus einem Typen, der die Input-Variablen der Abfrage beschreibt sowie mehreren Typen, die das Ergebnis der Abfrage beschreiben. Eine generierte Typ-Definition sieht beispielsweise wie folgt aus:

interface BeerPageQuery_beer_ratings { . . . }
interface BeerPageQuery_beer_shops { . . . }

interface BeerPageQuery_beer {
id: string;
name: string;
price: string;
ratings: BeerPageQuery_beer_ratings[];
shops: BeerPageQuery_beer_shops[];
}

export interface BeerPageQuery {
beer: BeerPageQuery_beer | null;
}

export interface BeerPageQueryVariables {
beerId: string;
}

Die BeerPageQuery liefert ein Feld mit dem Namen beer zurück, dass vom Typ BeerPageQuery_beer oder null sein kann. Das BeerPageQuery_beer-Objekt ist ebenfalls auf Basis der Schemainformationen entstanden, ebenso alle weiteren abgefragten (Unter-)Objekte. Die generierten Typen passen immer genau zu einer konkreten Query, das heißt sie enthalten nur die Felder, die die API tatsächlich abfragt, und nicht alle Felder, die die Schnittstelle für ein Objekt grundsätzlich bereitstellt. Da die generierten Typen die Dokumentation aus der Schemabeschreibung enthalten, kann man sie in der IDE zum Beispiel über Code-Completion ebenfalls anzeigen.

Zur Verwendung der Typen können Entwickler sowohl die Query- als auch die Mutation-Komponente von Apollo in TypeScript mit Typinformationen anreichern. Das folgende Listing zeigt exemplarisch die BeerPage-Komponente, die die BeerPageQuery samt der generierten Typen verwendet:

function BeerPage(props: BeerPageProps) {
return <div>
<Query<BeerPageQueryResult, BeerPageQueryVariables>
query={BEER_PAGE_QUERY}
// TS kennt hier den Typ von 'variables':
variables={{ beer: props.beerId }}>
{({ loading, error, data }) => {
if (loading) { . . . }
if (error) { . . . }

// TS kennt die 'data'-Struktur und erzwingt null Prüfung
if (!data || data.beer === null) {
return <h1>Beer Not found</h1>;
}

// TS kennt die Struktur des Query Ergebnisses ('data')
// z.B. dass es dort ein 'beer'-Property gibt und wie dieses aussieht

return <Beer beer={data.beer} />
}}
</Query>
</div>;
}

Durch die Angabe der Typen weiß der Compiler nun genau, wie das variables-Objekt aussehen muss, sodass zur Build-Zeit sichergestellt ist, dass alle erforderlichen Variablen gesetzt und auch vom korrekten Typ sind. Außerdem kennt der Compiler den Typ des Ergebnisses und stellt sicher, dass beim Arbeiten mit dem Ergebnis-Objekt in der Anwendung nur Felder zur Verwendung kommen, die tatsächlich abgefragt wurden, und dass sie vor der Verwendung gegebenenfalls auf null geprüft sind. Die Verwendung der Mutation-Komponente erfolgt analog.

TypeScript (hier in VS Code) erkennt Fehler bei der Verwendung von Queries und bietet Code-Completion bei der Arbeit mit Queries (Abb. 5)

Sofern sich das Schema oder die Queries ändern, können Entwickler die Typen jederzeit neu generieren und die Anwendung erneut kompilieren, um sicherzustellen, dass die Queries weiterhin zum Schema passen und die Anwendung die Queries korrekt verwendet.