topLeft lightmiddlePinkLightrightPurpleLight

Next.js + Clients Apollo multiples et sources GraphQL

Paul Grieselhuber

Paul Grieselhuber

Apr 23, 2019

Next.js + GraphQL s'avèrent être un moyen très efficace de construire des sites web statiques à petite et grande échelle, en particulier si vous utilisez les exportations HTML statiques de Next.js (voir my post on WPGraphQL pour un mode d'emploi sur ce sujet en utilisant GraphQL).

L'une des questions que de nombreux développeurs semblent se poser, et qui nécessite une quantité étonnante de recherches et de tests sur Google, est la construction d'un de ces sites avec deux sources de données GraphQL.

Un peu de contexte et de règles de base

Pour préparer le terrain à la solution, voici pourquoi il s'agit d'un petit défi. La configuration de base d'un client Apollo dans Next.js source implique trois fichiers :

  • Notre composant /pages/_app.js
  • /lib/init-apollo.js
  • /lib/with-apollo-client.js

Tout d'abord, jetons un coup d'œil à _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);

L'essentiel ici est que nous importons withApolloClient and wrap the rest of our app in that component, passing the apolloClient qui vient de props en tant que résultat. Cela permet à nos composants enfants d'accéder aux données provenant de la source GraphQL à laquelle Apollo est connecté. Continuons.

Dans init-apollo.js, nous créons le client Apollo et lui indiquons où se connecter :

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;
}

Ce qui se passe ici est assez simple, mais gardez un œil sur cette ligne de lien, nous y reviendrons...

Et enfin 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} />;
    }
  };
};

Ici, nous renvoyons deux versions différentes de notre composant HOC App (qui enveloppe ensuite les composants enfants dans _app.js) : la première est pour le mode SSR, la seconde est pour tout le reste.

Apollo attend un client

Vous avez probablement remarqué l'hypothèse implicite dans toute la logique ci-dessus qu'il y a précisément un client.

Le problème est que je veux plusieurs sources de données, et je veux les interroger dans le style prévu par les bibliothèques que j'utilise dans mon projet. Et je ne veux pas bidouiller quelque chose qui détruirait ma capacité à bénéficier de toutes les grandes fonctionnalités d'Apollo, etc.

L'une des options proposées était schema stitching, mais cela me semblait beaucoup plus compliqué que ce que je voulais faire juste pour pouvoir interroger deux clients ou plus.

Après avoir fait lots de discussions, fait lots de recherches, et refactorisé plus de fois que je ne voudrais l'admettre, j'ai réalisé que cela se résumait à ce que vous vouliez vraiment avoir en "multiple".

La solution

Quelle est donc la solution ? Est-ce que je veux plusieurs clients ? Plusieurs fournisseurs ?

Non, vous êtes loin du compte. Et probablement parce que je vous ai induit en erreur avec le titre. Ou peut-être parce que la bibliothèque Angular Apollo a cette option, et qu'elle se fait en fait avec plusieurs clients.

Mais nous sommes dans le monde React, et la réponse est : plusieurs liens dans votre seul client Apollo.

Heureusement, pour y parvenir, nous n'avons qu'à apporter des modifications mineures à deux de nos trois fichiers.

Dans 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 paramètre du client Apollo à la rescousse

Notez les changements effectués :

  • Nous avons supprimé notre premier lien, pour des raisons de lisibilité, puisque nous allons en créer un second (notez que vous pouvez passer toutes les options dont vous avez besoin à Apollo link modules ici)
  • Nous avons importé ApolloLink, dont la méthode disponible est .split(). Cette méthode prend au moins deux paramètres (bien que cela n'ait pas vraiment de sens d'en passer moins de trois) :
  • Le premier est une fonction permettant de décider quel lien utiliser
  • Le second est le lien à utiliser si cette fonction renvoie true
  • Le troisième est le lien à utiliser si la fonction renvoie false

À ce stade, vous avez probablement compris l'essentiel de ce qui se passe ici.

Rien de tel qu'un peu de contexte pour clarifier...

La question demeure cependant : comment dire à un composant quelle API GraphQL interroger ? En utilisant le contexte de la requête. Voici un exemple :

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

Note : J'utilise react-apollo-hooks' useQuery pour exécuter la requête, mais fonctionne exactement de la même manière en utilisant le composant Query si vous n'utilisez pas de react hooks.

L'intérêt de cet exemple est que nous pouvons définir la valeur du contexte comme nous le souhaitons dans notre requête, puis sur le site du réseau (dans notre méthode ApolloLink à init-apollo.js). Assurez-vous simplement d'utiliser les mêmes valeurs aux deux endroits.

J'aime bien configurer le lien de mon client Apollo comme suit :

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

Notez que le "firstLink" est affiché en second, ce qui en fait la source de données par défaut à utiliser, si la valeur de contexte définie dans la requête ne correspond pas à la valeur recherchée par la fonction split. Cela signifie que nous ne devons définir le contexte que lorsque nous voulons utiliser notre client "autre que celui par défaut".

Mise à jour de with-apollo-client pour nous donner des exportations HTML entièrement rendues

Ok, nous entrons dans la dernière ligne droite, tout ce que nous avons à faire est de modifier notre with-apollo-client.js pour qu'il corresponde à ce qui suit :

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} />;
    }
  };
};

Notez que les seuls changements / ajouts sont les deux importations finales (getMarkupFromTree & renderToString), et l'ajout de la section getMarkupFromTree, qui nous permet d'avoir des exportations HTML statiques entièrement rendues à partir de Next.js.

Mais que faire si j'ai besoin de plus de deux sources de données dans mon application ?

Eh bien, c'est drôle que vous posiez la question, parce que l'astuce ci-dessus (la section .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.

Ce deuxième lien peut être un autre ApolloLink ! Comme vous pouvez le voir ci-dessous, nous pouvons continuer à enchaîner les liens jusqu'à ce que nous ayons vérifié le contexte de toutes les sources de données possibles :

// 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 : mais qu'en est-il des requêtes multiples ?

Si vous vous connectez à plusieurs clients, il y a de fortes chances que quelque part dans votre projet vous ayez besoin d'exécuter deux requêtes dans le même composant. Bien sûr, nous pourrions importer compose de react-apollo dans notre projet, mais ce n'est pas vraiment nécessaire dans ce cas (source)). Nous pouvons simplement créer notre propre hook (si vous utilisez les hooks de React). Cela ressemble à quelque chose comme :

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;

Chapeau bas à @ThomasK33 pour son aide dans la résolution de ce problème.

Bonus #2 : Qu'en est-il de la mutation sur plusieurs sources ?

Depuis la dernière mise à jour ici (et je mettrai à jour ce billet si la situation évolue), la réponse semble être "ça dépend".

Si vous utilisez un HOC <Mutation>, vous pouvez facilement passer le contexte à l'opération (docs), et sélectionner votre source de données de la même manière que nous l'avons fait ci-dessus.

Cependant, si vous souhaitez passer le contexte à useMutation à partir de la bibliothèque non officielle react-apollo-hooks, il semble que vous n'ayez pas de chance pour l'instant. La bonne nouvelle est que la bibliothèque officielle Apollo React Hooks est actuellement dans beta, et il semble certain qu'ils incluront le contexte comme une propriété dans leur version de useMutation, donc une solution ici est probablement sur le point d'arriver.

Paul Grieselhuber

Paul Grieselhuber

Founder, President

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

Commentaires

    Réservez un appel de découverte avec nos experts produits.

    Notre équipe d'experts en applications web et mobiles est impatiente de discuter avec vous de votre prochain projet.

    Réservez un appel avec nous 👋