Relay vs. Apollo Client: Wann lohnt sich Relays strenge Konventionen
AI generated
{ }
type
GraphQL · React · Client · Relay
Relay vs. Apollo Client
Strenge Konventionen gegen Flexibilität, und wann sich welcher Weg auszahlt

Relay erzwingt über einen eigenen Compiler strenge Namenskonventionen, Fragment-Colocation und eine feste Connection-Spezifikation für Pagination. Apollo Client verzichtet auf diesen Zwang und bleibt dadurch flexibler, aber auch weniger vorhersehbar bei wachsender Teamgröße. Dieser Artikel zeigt, wann sich Relays Lernkurve tatsächlich auszahlt.

16 Min. Lesezeit Fragment-Colocation · Compiler React · Relay · Apollo Client

1. React-GraphQL-Clients im Überblick: warum die Wahl über Jahre nachwirkt

Ein GraphQL-Client entscheidet nicht nur, wie Daten aus einer API geladen werden, sondern prägt über Jahre hinweg, wie ein React-Team Komponenten strukturiert, Caching-Verhalten versteht und mit wachsender Codebasis umgeht. Relay und Apollo Client sind die beiden etabliertesten Optionen, verfolgen dabei aber fast entgegengesetzte Philosophien: Relay erzwingt Struktur über einen Compiler, Apollo Client überlässt Struktur weitgehend dem Team.

Beide Clients lösen dasselbe Grundproblem, nämlich Daten normalisiert zu cachen und Komponenten nur mit den Feldern zu versorgen, die sie tatsächlich benötigen, kommen dabei aber zu unterschiedlichen Antworten auf die Frage, wie viel Freiheit ein Entwicklerteam beim Schreiben von Queries haben sollte.

2. Relay: Fragment-Colocation und der Relay-Compiler

Relays zentrales Prinzip ist die Fragment-Colocation: Jede Komponente deklariert über ein GraphQL-Fragment exakt die Felder, die sie selbst benötigt, direkt im selben Datei wie die Komponente. Der Relay-Compiler läuft zur Build-Zeit, sammelt alle Fragmente im Baum ein, fügt sie zu einer einzigen optimierten Query zusammen und generiert dabei streng typisierte TypeScript-Typen für jede Komponente.

Diese Colocation verhindert ein klassisches Problem großer GraphQL-Apps, das sogenannte Overfetching durch Fernbedienung: Eine Elternkomponente muss nicht mehr wissen, welche Felder eine tief verschachtelte Kindkomponente benötigt, weil das Kind sein eigenes Fragment mitbringt. Der Compiler erzwingt dabei zugleich strikte Namenskonventionen, etwa dass jedes Fragment nach dem Muster KomponentenName_propName benannt werden muss, was Builds bei Verstößen hart fehlschlagen lässt.


import { graphql, useFragment } from 'react-relay'
import type { ProductCard_product$key } from './__generated__/ProductCard_product.graphql'

const productFragment = graphql`
  fragment ProductCard_product on Product {
    id
    name
    price
  }
`

function ProductCard({ product }: { product: ProductCard_product$key }) {
  const data = useFragment(productFragment, product)
  return (
    <div>
      <h3>{data.name}</h3>
      <p>{data.price} EUR</p>
    </div>
  )
}

3. Die Connection-Spezifikation: Relays feste Regeln für Pagination

Relay verlangt für paginierte Listen die sogenannte Connection-Spezifikation: ein Feld muss ein edges-Array mit node und cursor pro Eintrag sowie ein pageInfo-Objekt mit hasNextPage und endCursor zurückgeben. Backends müssen diese Struktur exakt einhalten, damit Relays eingebaute usePaginationFragment-Hooks automatisch funktionieren, inklusive korrektem Cache-Merging neuer Seiten in bereits geladene Listen.

Diese Striktheit hat einen klaren Vorteil: Sobald ein Schema der Connection-Spezifikation folgt, funktioniert Pagination in jeder Relay-Komponente identisch, ohne dass ein Team jedes Mal neu entscheiden muss, wie Cursor gehandhabt oder neue Seiten an bestehende Listen angehängt werden. Der Nachteil ist, dass ein bestehendes Schema, das diese Struktur nicht bereits verwendet, erst angepasst werden muss, bevor Relay sinnvoll eingesetzt werden kann.


type Query {
  products(first: Int, after: String): ProductConnection!
}

type ProductConnection {
  edges: [ProductEdge!]!
  pageInfo: PageInfo!
}

type ProductEdge {
  node: Product!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

4. Apollo Client: Flexibilität ohne Compiler-Zwang

Apollo Client verzichtet bewusst auf einen Build-Time-Compiler und lässt Teams Queries direkt mit useQuery oder useFragment in Komponenten schreiben, ohne feste Namenskonventionen oder eine vorgeschriebene Connection-Struktur. Der normalisierte InMemoryCache arbeitet dabei nach demselben Grundprinzip wie Relays Store, cacht Objekte nach Typ und ID, verlangt aber keine Compiler-generierten Artefakte, um zu funktionieren.

Diese Flexibilität senkt die Einstiegshürde erheblich: Ein neues Teammitglied kann am ersten Tag produktiv eine Query schreiben, ohne den Relay-Compiler, generierte Typdateien oder eine feste Fragment-Namenskonvention verstehen zu müssen. Der Preis dafür zeigt sich erst mit wachsender Teamgröße, wenn unterschiedliche Entwickler unterschiedliche Muster für dieselben Aufgaben wie Pagination oder Cache-Updates etablieren.


import { gql, useFragment } from '@apollo/client'

const PRODUCT_CARD_FRAGMENT = gql`
  fragment ProductCardFields on Product {
    id
    name
    price
  }
`

function ProductCard({ productRef }) {
  const { data } = useFragment({
    fragment: PRODUCT_CARD_FRAGMENT,
    from: productRef,
  })
  return (
    <div>
      <h3>{data.name}</h3>
      <p>{data.price} EUR</p>
    </div>
  )
}

5. Fragment-Colocation in Apollo Client vs. Relays Zwang

Apollo Client unterstützt Fragment-Colocation ebenfalls über useFragment und das Babel- oder SWC-Plugin für Fragment-Masking, allerdings als Option, nicht als Zwang. Ein Team kann jederzeit direkt in einer Elternkomponente auf Felder eines Kindfragments zugreifen, was Relay durch striktes Fragment-Masking technisch verhindert, wo ein Fragment-Ergebnis nur über useFragment und ausschließlich innerhalb der besitzenden Komponente lesbar ist.

Dieser Unterschied wirkt sich direkt auf die langfristige Wartbarkeit aus: Relays erzwungenes Fragment-Masking verhindert versehentliche Kopplung zwischen Komponenten zuverlässig, während Apollo Clients optionales Masking dieselbe Disziplin nur dann bringt, wenn ein Team sie sich selbst als Konvention auferlegt und im Code-Review durchsetzt.

6. Lernkurve und Onboarding: Compiler-Fehler als Frühwarnsystem

Relays Lernkurve ist unbestreitbar steiler: Neue Entwickler müssen Konzepte wie Fragment-Colocation, die Connection-Spezifikation, generierte Artefakte und die Trennung zwischen useLazyLoadQuery und useFragment verstehen, bevor sie produktiv Komponenten schreiben können. Ein typisches Onboarding dauert bei Relay spürbar länger als bei Apollo Client, wo die grundlegenden Hooks fast ohne Vorwissen nutzbar sind.

Diese Steilheit hat jedoch einen oft unterschätzten Vorteil: Der Relay-Compiler schlägt bei fast jeder strukturellen Inkonsistenz, etwa einem fehlenden Fragment-Spread oder einem falsch benannten Fragment, schon beim Build fehl, statt erst zur Laufzeit einen unklaren Fehler zu zeigen. Dieses Frühwarnsystem fängt genau die Klasse von Fehlern ab, die in großen Apollo-Client-Codebasen oft erst in Code-Reviews oder gar erst in Produktion auffallen.

7. Wann sich Relays Strenge für große Teams und Apps auszahlt

Relays Investition zahlt sich vor allem dann aus, wenn viele Entwickler parallel an derselben Codebasis arbeiten, Komponenten tief verschachtelt sind und Konsistenz bei Pagination, Caching und Datenzugriff über Teamgrenzen hinweg entscheidend ist. In solchen Umgebungen verhindert der Compiler genau die Art von stillschweigender Divergenz, die bei Apollo Client erst durch strenge interne Konventionen und diszipliniertes Review erreicht werden müsste.

Facebook selbst entwickelte Relay ursprünglich für genau dieses Szenario: Zehntausende Komponenten, Hunderte Entwickler, ein einziges Schema. Für ein Team dieser Größenordnung ist der einmalige Aufwand, den Compiler zu etablieren und alle Entwickler in die Connection-Spezifikation einzuarbeiten, meist kleiner als die langfristigen Kosten uneinheitlicher Query-Patterns.


import { graphql, usePaginationFragment } from 'react-relay'

const productListFragment = graphql`
  fragment ProductList_query on Query
  @refetchable(queryName: "ProductListPaginationQuery") {
    products(first: $count, after: $cursor)
      @connection(key: "ProductList_products") {
      edges {
        node {
          id
          name
        }
      }
    }
  }
`

function ProductList({ query }) {
  const { data, loadNext, hasNext } = usePaginationFragment(
    productListFragment,
    query,
  )
  return (
    <>
      {data.products.edges.map(({ node }) => <div key={node.id}>{node.name}</div>)}
      {hasNext && <button onClick={() => loadNext(10)}>Load more</button>}
    </>
  )
}

8. Wann Relay überdimensioniert ist: kleine Projekte und Prototyping

Für ein kleines Team, ein Prototyp-Projekt oder eine App mit überschaubarer Komponentenanzahl steht der Einrichtungsaufwand von Relay, inklusive Compiler-Setup, Connection-konformem Backend und Einarbeitungszeit, in keinem sinnvollen Verhältnis zum Nutzen. Apollo Client liefert in solchen Fällen dieselbe grundlegende Cache-Funktionalität ohne Build-Time-Abhängigkeit und lässt sich in wenigen Minuten in ein bestehendes Projekt integrieren.

Auch wenn das Backend-Schema die Connection-Spezifikation nicht bereits nativ unterstützt, etwa bei einer bestehenden GraphQL-Mesh-Integration über ein Fremdsystem, ist der Migrationsaufwand zu Relay oft unverhältnismäßig hoch im Vergleich zum Mehrwert. In solchen Fällen bleibt Apollo Client die pragmatischere Wahl, selbst wenn das Team langfristig wächst.

9. Entscheidungshilfe: Relay oder Apollo Client für euer Projekt

Die Entscheidung hängt letztlich weniger von der reinen Funktionalität ab, die sich bei beiden Clients stark überschneidet, sondern davon, ob ein Team von erzwungener Struktur profitiert oder durch sie ausgebremst wird. Große, langlebige Anwendungen mit vielen parallel arbeitenden Teams profitieren meist von Relays Zwang, während kleinere oder schneller iterierende Projekte mit Apollo Clients Flexibilität besser fahren.

Die folgende Tabelle fasst die wichtigsten Unterschiede zusammen.

Kriterium Relay Apollo Client Empfehlung
Fragment-Colocation Erzwungen, Compiler-geprüft Optional, konventionsbasiert Relay bei großen Teams
Pagination Feste Connection-Spezifikation Frei wählbares Muster Relay bei Konsistenz-Bedarf
Einstiegshürde Hoch, Compiler-Setup nötig Niedrig, sofort produktiv Apollo bei kleinen Teams
Backend-Anforderung Connection-konformes Schema Keine feste Struktur nötig Apollo bei heterogenen Schemas

Mironsoft

GraphQL-Schema-Design, Resolver-Performance und API-Sicherheit

GraphQL-APIs, die unter echter Last stabil bleiben?

Wir prüfen bestehende GraphQL-Schemas und Resolver, decken N+1-Probleme und fehlende Query-Limits auf und bauen daraus eine API, die Performance, Sicherheit und Wartbarkeit gleichzeitig hält.

Schema-Review

Typen, Resolver und Berechtigungen auf Konsistenz und Sicherheitslücken prüfen.

Performance-Optimierung

DataLoader, Caching und Query-Complexity-Limits gegen N+1 und Overfetching einsetzen.

Produktions-Absicherung

Rate-Limiting, Introspection-Schutz und Monitoring für den produktiven Betrieb einrichten.

10. Zusammenfassung

Relay und Apollo Client: Das Wichtigste auf einen Blick

Fragment-Colocation

Relay erzwingt sie über den Compiler und generierte Typen, Apollo Client unterstützt sie optional ohne Build-Time-Zwang.

Pagination

Relays Connection-Spezifikation liefert einheitliches Verhalten in jeder Komponente, Apollo Client überlässt das Muster dem Team.

Lernkurve

Relay ist steiler beim Einstieg, fängt strukturelle Fehler aber schon beim Build ab statt erst zur Laufzeit.

Passende Teamgröße

Relays Strenge zahlt sich bei großen, langlebigen Apps mit vielen Entwicklern aus, Apollo Client bei kleineren, schneller iterierenden Projekten.

11. FAQ: Relay und Apollo Client: Das Wichtigste auf einen Blick

1Was ist der größte konzeptionelle Unterschied zwischen Relay und Apollo Client?
Relay erzwingt über einen Compiler strenge Namenskonventionen, Fragment-Colocation und eine feste Connection-Spezifikation für Pagination. Apollo Client überlässt diese Entscheidungen weitgehend dem Team und verzichtet auf einen Build-Time-Compiler.
2Brauche ich für Relay ein spezielles Backend-Schema?
Ja, zumindest für Pagination. Relay verlangt, dass paginierte Felder der Connection-Spezifikation mit edges, node, cursor und pageInfo folgen, damit die eingebauten Pagination-Hooks funktionieren.
3Ist Apollo Client einfacher für Einsteiger?
Ja. Ohne Compiler, generierte Artefakte oder feste Namenskonventionen können neue Entwickler mit Apollo Client meist am ersten Tag produktiv Queries schreiben.
4Was ist Fragment-Masking bei Relay?
Fragment-Masking bedeutet, dass ein Fragment-Ergebnis nur innerhalb der Komponente lesbar ist, die es über useFragment anfordert. Andere Komponenten können nicht versehentlich auf diese Felder zugreifen, was Kopplung verhindert.
5Kann Apollo Client Fragment-Masking auch erzwingen?
Teilweise, über ein Babel- oder SWC-Plugin für Fragment-Masking, aber es ist eine Option, die ein Team aktiv einrichten und im Review durchsetzen muss, keine Standardeinstellung.
6Wann lohnt sich der Umstieg von Apollo Client zu Relay?
Meist erst bei wachsender Teamgröße und tief verschachtelten Komponentenbäumen, wenn uneinheitliche Query-Patterns zu einem echten Wartungsproblem werden. Für kleine bis mittlere Projekte überwiegt der Migrationsaufwand oft den Nutzen.
7Generiert Relay TypeScript-Typen automatisch?
Ja, der Relay-Compiler generiert für jedes Fragment und jede Query strikt typisierte TypeScript-Definitionen automatisch, basierend auf dem GraphQL-Schema.
8Unterstützt Apollo Client auch normalisiertes Caching?
Ja, der InMemoryCache von Apollo Client normalisiert Objekte nach Typ und ID nach demselben Grundprinzip wie Relays Store, ohne dafür Compiler-generierte Artefakte zu benötigen.
9Kann ich in einem Projekt Relay und Apollo Client mischen?
Technisch möglich, aber in der Praxis unüblich und wartungsaufwendig, weil beide Clients einen eigenen, inkompatiblen normalisierten Cache pflegen. Für ein Projekt sollte man sich auf einen Client festlegen.
10Funktioniert Relay auch außerhalb von React?
Nein, Relay ist eng an React und seine Hooks gebunden. Apollo Client bietet dagegen auch Anbindungen für Vue, Angular und reines JavaScript neben der React-Integration.