Next.js + Mehrere Apollo Clients & GraphQL-Quellen

Paul Grieselhuber

Paul Grieselhuber

Apr 23, 2019

Next.js + GraphQL ist eine hervorragende Möglichkeit, sowohl kleine als auch große statische Websites zu erstellen, vor allem, wenn man die statischen HTML-Exporte von Next.js nutzt (siehe my post on WPGraphQL für eine Anleitung zu diesem Thema mit GraphQL).

Eine der Fragen, die viele Entwickler zu haben scheinen, und die eine erstaunliche Menge an Googeln, Recherchen und Tests erfordert, um sie zu klären, ist der Aufbau einer dieser Websites mit zwei GraphQL-Datenquellen.

Ein wenig Hintergrund & Standardwerk

Um den Rahmen für die Lösung abzustecken, hier der Grund, warum dies eine kleine Herausforderung darstellt. Das grundlegende Setup für einen Apollo-Client in Next.js source umfasst drei Dateien:

  • Unsere Komponente /pages/_app.js
  • /lib/init-apollo.js
  • /lib/with-apollo-client.js

Werfen wir zunächst einen Blick auf _app.js:

import App, { Container } from "next/app";
import React from "react";
import withApolloClient from "../lib/with-apollo-client";
import { ApolloProvider } from "react-apollo";

class MyApp extends App {
  render() {
    const { Component, pageProps, apolloClient } = this.props;
    return (
      <Container>
        <ApolloProvider client={apolloClient}>
          <Component {...pageProps} />
        </ApolloProvider>
      </Container>
    );
  }
}

export default withApolloClient(MyApp);

Das Wesentliche hier ist, dass wir withApolloClient and wrap the rest of our app in that component, passing the apolloClient importieren, das von props als Ergebnis mitkommt. Dadurch erhalten unsere untergeordneten Komponenten Zugriff auf die Daten, die von der GraphQL-Quelle stammen, mit der Apollo verbunden ist. Weiter geht's.

In init-apollo.js erstellen wir den Apollo-Client und sagen ihm, wo er sich verbinden soll:

import { ApolloClient, InMemoryCache, HttpLink } from "apollo-boost";
import fetch from "isomorphic-unfetch";

let apolloClient = null;

// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
  global.fetch = fetch;
}

function create(initialState) {
  // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
  return new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
    link: new HttpLink({
      uri: "https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn", // Server URL (must be absolute)
      credentials: "same-origin", // Additional fetch() options like `credentials` or `headers`
    }),
    cache: new InMemoryCache().restore(initialState || {}),
  });
}

export default function initApollo(initialState) {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (!process.browser) {
    return create(initialState);
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = create(initialState);
  }

  return apolloClient;
}

Ziemlich klar, was hier vor sich geht, aber behalten Sie die Link-Zeile im Auge, wir werden darauf zurückkommen...

Und schließlich with-apollo-client.js:

import React from "react";
import initApollo from "./init-apollo";
import Head from "next/head";
import { getDataFromTree } from "react-apollo";

export default (App) => {
  return class Apollo extends React.Component {
    static displayName = "withApollo(App)";
    static async getInitialProps(ctx) {
      const { Component, router } = ctx;

      let appProps = {};
      if (App.getInitialProps) {
        appProps = await App.getInitialProps(ctx);
      }

      // Run all GraphQL queries in the component tree
      // and extract the resulting data
      const apollo = initApollo();
      if (!process.browser) {
        try {
          // Run all GraphQL queries
          await getDataFromTree(
            <App
              {...appProps}
              Component={Component}
              router={router}
              apolloClient={apollo}
            />
          );
        } catch (error) {
          // Prevent Apollo Client GraphQL errors from crashing SSR.
          // Handle them in components via the data.error prop:
          // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
          console.error("Error while running `getDataFromTree`", error);
        }

        // getDataFromTree does not call componentWillUnmount
        // head side effect therefore need to be cleared manually
        Head.rewind();
      }

      // Extract query data from the Apollo store
      const apolloState = apollo.cache.extract();

      return {
        ...appProps,
        apolloState,
      };
    }

    constructor(props) {
      super(props);
      this.apolloClient = initApollo(props.apolloState);
    }

    render() {
      return <App {...this.props} apolloClient={this.apolloClient} />;
    }
  };
};

Hier geben wir zwei verschiedene Versionen unserer HOC-App-Komponente zurück (die später die untergeordneten Komponenten in _app.js umhüllt): die erste ist für den SSR-Modus, die zweite für alles andere.

Apollo erwartet einen Client

Sie haben wahrscheinlich die implizite Annahme in der gesamten obigen Logik bemerkt, dass es genau einen Client gibt.

Das Problem ist, dass ich mehrere Datenquellen haben möchte, und ich möchte sie in dem Stil abfragen, den die Bibliotheken, die ich in meinem Projekt verwende, vorsehen. Und ich möchte nicht etwas zusammenhacken, das mir die Möglichkeit nimmt, von all den großartigen Funktionen von Apollo zu profitieren usw.

Eine Option, die mir in den Sinn kam, war schema stitching, aber das schien mir einfach zu viel Aufwand zu sein, nur um zwei oder mehr Clients abfragen zu können.

Nachdem ich viele Diskussionen, viele Recherchen und mehr Refactoring durchgeführt hatte, als ich zugeben möchte, wurde mir klar, dass es darauf ankommt, wovon man wirklich "mehrere" haben möchte.

Die Lösung

Wie lautet also die Antwort auf diese Frage? Möchte ich mehrere Clients? Mehrere Anbieter?

Nein, da liegen Sie völlig falsch. Und wahrscheinlich, weil ich Sie mit dem Titel in die Irre geführt habe. Oder vielleicht, weil die Angular Apollo-Bibliothek diese Option hat, und es ist in der Tat mit mehreren Clients getan.

Aber wir sind in der React-Welt, und die Antwort ist: mehrere Links in Ihrem einen Apollo-Client.

Um das zu erreichen, müssen wir glücklicherweise nur kleine Änderungen an zwei unserer drei Dateien vornehmen.

In init-apollo.js:

import { ApolloLink } from "apollo-link";
import { ApolloClient, InMemoryCache, HttpLink } from "apollo-boost";
import fetch from "isomorphic-unfetch";

let apolloClient = null;

// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
  global.fetch = fetch;
}

// Create First Link
const firstLink = new HttpLink({
  uri: "https://www.firstsource.com/api",
  headers: yourHeadersHere,
  // other link options...
});

// Create Second Link
const secondLink = new HttpLink({
  uri: "https://www.secondsource.com/api",
  headers: yourHeadersHere,
  // other link options...
});

function create(initialState) {
  // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
  return new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
    link: ApolloLink.split(
      (operation) => operation.getContext().clientName === "second", // Routes the query to the proper client
      secondLink,
      firstLink
    ),
    cache: new InMemoryCache().restore(initialState || {}),
  });
}

export default function initApollo(initialState) {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (!process.browser) {
    return create(initialState);
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = create(initialState);
  }

  return apolloClient;
}

link Parameter des Apollo Client zur Rettung

Beachten Sie die vorgenommenen Änderungen:

  • Wir haben unseren ersten Link aus Gründen der Lesbarkeit ausgeklammert, da wir einen zweiten erstellen werden (beachten Sie, dass Sie hier beliebige Optionen an Apollo link modules übergeben können)
  • Wir haben ApolloLink importiert, das als verfügbare Methode .split() hat. Diese Methode benötigt mindestens zwei Parameter (obwohl es nicht wirklich sinnvoll ist, weniger als drei zu übergeben):
  • Der erste ist eine Funktion, die entscheidet, welcher Link verwendet werden soll
  • Der zweite ist der zu verwendende Link, wenn diese Funktion true zurückgibt.
  • Der dritte ist der zu verwendende Link, wenn die Funktion false zurückgibt.
  • An dieser Stelle haben Sie wahrscheinlich das Wesentliche verstanden, was hier vor sich geht.

An dieser Stelle haben Sie wahrscheinlich das Wesentliche verstanden, was hier vor sich geht.

Es geht nichts über ein bisschen Kontext, um zu verdeutlichen, was hier passiert...

Es bleibt jedoch die Frage, wie wir einer Komponente mitteilen, welche GraphQL-API sie abfragen soll? Indem wir den Kontext der Abfrage verwenden. Hier ist ein Beispiel:

const { data, error, loading } = useQuery(GET_STUFF, {
  context: { clientName: "second" },
});

Hinweis: Ich verwende react-apollo-hooks' useQuery, um die Abfrage auszuführen, aber funktioniert genauso mit der Komponente Query, wenn Sie keine React-Hooks verwenden.

Der Sinn dieses Beispiels ist also, dass wir den Wert von context in unserer Abfrage und dann auf der Netzwerkseite (in unserer ApolloLink-Methode in init-apollo.js) auf jeden beliebigen Wert setzen können. Achten Sie nur darauf, dass Sie an beiden Stellen die gleichen Werte verwenden.

Ich möchte den Link meines Apollo-Clients wie folgt einrichten:

// ...stuff
    link: ApolloLink.split(
      operation => operation.getContext().clientName === "second", // Routes the query to the proper client
      secondLink,
      firstLink
    ),
// ...stuff

Beachten Sie, dass der "firstLink" an zweiter Stelle angezeigt wird, was ihn zur Standarddatenquelle macht, die verwendet wird, wenn der in der Abfrage eingestellte Kontextwert nicht mit dem Wert übereinstimmt, nach dem die Funktion split sucht. Das bedeutet, dass wir den Kontext nur dann setzen müssen, wenn wir unseren "Nicht-Standard"-Mandanten verwenden wollen.

Aktualisierung von with-apollo-client, damit wir vollständig gerenderte HTML-Exporte erhalten

Ok, wir kommen jetzt auf die Zielgerade, alles was wir tun müssen, ist unsere with-apollo-client.js so zu bearbeiten, dass sie dem Folgenden entspricht:

import React from "react";
import initApollo from "./init-apollo";
import Head from "next/head";
import { getDataFromTree } from "react-apollo";
import { getMarkupFromTree } from "react-apollo-hooks";
import { renderToString } from "react-dom/server";

export default (App) => {
  return class Apollo extends React.Component {
    static displayName = "withApollo(App)";
    static async getInitialProps(ctx) {
      const { Component, router } = ctx;

      let appProps = {};
      if (App.getInitialProps) {
        appProps = await App.getInitialProps(ctx);
      }

      // Run all GraphQL queries in the component tree
      // and extract the resulting data
      const apollo = initApollo();
      if (!process.browser) {
        try {
          // Run all GraphQL queries
          await getDataFromTree(
            <App
              {...appProps}
              Component={Component}
              router={router}
              apolloClient={apollo}
            />
          );
          // Create static markup for SSR HTML export
          await getMarkupFromTree({
            renderFunction: renderToString,
            tree: (
              <App
                {...appProps}
                Component={Component}
                router={router}
                apolloClient={apollo}
              />
            ),
          });
        } catch (error) {
          // Prevent Apollo Client GraphQL errors from crashing SSR.
          // Handle them in components via the data.error prop:
          // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
          console.error("Error while running `getDataFromTree`", error);
        }

        // getDataFromTree does not call componentWillUnmount
        // head side effect therefore need to be cleared manually
        Head.rewind();
      }

      // Extract query data from the Apollo store
      const apolloState = apollo.cache.extract();

      return {
        ...appProps,
        apolloState,
      };
    }

    constructor(props) {
      super(props);
      this.apolloClient = initApollo(props.apolloState);
    }

    render() {
      return <App {...this.props} apolloClient={this.apolloClient} />;
    }
  };
};

Die einzigen Änderungen/Ergänzungen sind die beiden endgültigen Importe (getMarkupFromTree & renderToString) und die Hinzufügung des Abschnitts getMarkupFromTree, der es uns ermöglicht, vollständig gerenderte statische HTML-Exporte von Next.js zu haben.

Was aber, wenn ich mehr als zwei Datenquellen in meiner Anwendung benötige?

Witzig, dass Sie das fragen, denn der obige Trick (die .split() operation on ApolloLink) only works with up to two links. Bit of a chin scratcher at first, until you realize that the second link passed to the .split method does not have to be an HttpLink.

Dieser zweite Link kann ein weiterer ApolloLink sein! Wie Sie unten sehen können, können wir einfach Links aneinanderreihen, bis wir den Kontext für alle möglichen Datenquellen überprüft haben:

// Create First Link
const firstLink = new HttpLink({
  uri: "https://www.firstsource.com/api",
  headers: yourHeadersHere,
  // other link options...
});

// Create Second Link
const secondLink = new HttpLink({
  uri: "https://www.secondsource.com/api",
  headers: yourHeadersHere,
  // other link options...
});

// Create Third Link
const thirdLink = new HttpLink({
  uri: "https://www.thirdsource.com/api",
  headers: yourHeadersHere,
  // other link options...
});

const otherLinks = ApolloLink.split(
  (operation) => operation.getContext().clientName === "third", // Routes the query to the proper client
  thirdLink,
  firstLink
);

function create(initialState) {
  const client = new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
    link: ApolloLink.split(
      (operation) => operation.getContext().clientName === "second", // Routes the query to the proper client
      secondLink,
      otherLinks
    ),
    cache: new InMemoryCache({ fragmentMatcher }).restore(initialState || {}),
  });
  return client;
}

Bonus: aber was ist mit mehreren Abfragen?

Wenn Sie sich mit mehreren Clients verbinden, ist es wahrscheinlich, dass Sie irgendwo in Ihrem Projekt zwei Abfragen in derselben Komponente ausführen müssen. Sicher, wir könnten compose von react-apollo in unser Projekt importieren, aber das ist in diesem Fall wirklich nicht nötig (source). Wir können einfach unseren eigenen Hook erstellen (wenn Sie React Hooks verwenden). Das sieht dann in etwa so aus:

const queryMultiple = () => {
  const res = useQuery(GET_POST, {
    variables: {
      uri: props.uri,
    },
  });
  const res2 = useQuery(GET_SHOP, {
    context: { clientName: "second" },
  });
  return [res, res2];
};

const [res, res2] = queryMultiple();
const { data, error, loading } = res;
const { data: secondData, error: secondError, loading: secondLoading } = res2;

Hut ab vor @ThomasK33 für seine Hilfe, dies herauszufinden.

Bonus #2: Was ist mit der Mutation bei mehreren Quellen?

Nach dem letzten Update hier (und ich werde diesen Beitrag aktualisieren, wenn sich die Situation ändert), scheint die Antwort zu sein: "Es kommt darauf an".

Wenn Sie ein <Mutation>HOC verwenden, können Sie einfach Kontext an die Operation übergeben (docs) und Ihre Datenquelle auf ähnliche Weise auswählen, wie wir es oben getan haben.

Wenn Sie jedoch Kontext an useMutation aus der inoffiziellen react-apollo-hooks Bibliothek übergeben wollen, scheinen Sie derzeit kein Glück zu haben. Die gute Nachricht ist, dass die offizielle Apollo React Hooks Bibliothek derzeit in beta ist, und es scheint eine Gewissheit zu sein, dass sie Kontext als eine Eigenschaft in ihrer Version von useMutation einschließen würde, so dass eine Lösung hier wahrscheinlich um die Ecke ist.

Paul Grieselhuber

Paul Grieselhuber

Founder, President

Paul has extensive background in software development and product design. Currently he runs rendr.

Buchen Sie ein Informationsgespräch mit unseren Produktexperten.

Unser Team von Experten für Web- und mobile Anwendungen freut sich darauf, Ihr nächstes Projekt mit Ihnen zu besprechen.

Buchen Sie einen Anruf 👋.