topLeft lightmiddlePinkLightrightPurpleLight

Next.js + più client Apollo e sorgenti GraphQL

Paul Grieselhuber

Paul Grieselhuber

Apr 23, 2019

Next.js + GraphQL si è rivelato un modo piuttosto straordinario per costruire siti web statici sia di piccole che di grandi dimensioni, soprattutto se si fa uso delle esportazioni HTML statiche di Next.js (si veda il post di my su WPGraphQL per un how-to su questo usando GraphQL).

Una delle domande che molti sviluppatori sembrano avere e che richiede una quantità sorprendente di ricerche, ricerche e test per essere risolta è la costruzione di uno di questi siti con due sorgenti di dati GraphQL.

Un po' di background e boilerplate

Per preparare il terreno per la soluzione, ecco il motivo per cui questa è un po' una sfida. La configurazione di base per un client Apollo in Next.js source prevede tre file:

  • Il nostro componente /pages/_app.js
  • /lib/init-apollo.js
  • /lib/con-apollo-client.js

Innanzitutto, diamo un'occhiata a _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);

La sostanza è che importiamo withApolloClient and wrap the rest of our app in that component, passing the apolloClient che viene fornito da props come risultato. Questo dà ai nostri componenti figli l'accesso ai dati provenienti dalla sorgente GraphQL a cui Apollo è collegato. Avanti.

In init-apollo.js, creiamo il client Apollo e gli diciamo dove connettersi:

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

È abbastanza chiaro quello che succede qui, ma tenete d'occhio la linea di collegamento, ci torneremo...

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

Qui restituiamo due versioni diverse del nostro componente HOC App (che poi avvolge i componenti figli in _app.js): la prima è per la modalità SSR, la seconda per tutto il resto.

Apollo si aspetta un client

Probabilmente si è notato che in tutta la logica precedente si assume implicitamente che ci sia un solo client.

Il problema è che voglio più fonti di dati e voglio interrogarle nello stile previsto dalle librerie che sto usando nel mio progetto. Inoltre, non voglio creare qualcosa che distrugga la mia capacità di trarre vantaggio da tutte le fantastiche funzionalità di Apollo, ecc.

Un'opzione che mi è venuta in mente è schema stitching, ma mi è sembrato molto di più di quanto volessi fare solo per poter interrogare due o più client.

Dopo aver fatto lotti di discussioni, aver fatto lotti di ricerche e aver rifattorizzato più volte di quanto vorrei ammettere, ho capito che si tratta di capire di cosa si vuole veramente avere "multipli".

La soluzione

Qual è la risposta? Voglio più clienti? Più fornitori?

No, sei fuori strada. E probabilmente perché vi ho fuorviato con il titolo. O forse perché la libreria Angular Apollo ha questa opzione, ed è in effetti realizzata con più client.

Ma siamo nel mondo di React e la risposta è: più link in un unico client Apollo.

Fortunatamente per arrivarci dobbiamo apportare solo piccole modifiche a due dei nostri tre file.

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 parametro del client Apollo in soccorso

Si notino le modifiche apportate:

  • Abbiamo eliminato il nostro primo collegamento, per motivi di leggibilità, dato che ne creeremo un secondo (si noti che si può passare qualsiasi opzione necessaria a Moduli di collegamento Apollo qui)
  • Abbiamo importato ApolloLink, che ha come metodo .split(). Questo metodo richiede almeno due parametri (anche se non ha senso passarne meno di tre):
  • Il primo è una funzione che decide quale link usare
  • Il secondo è il collegamento da usare se questa funzione restituisce true
  • Il terzo è il link da usare se la funzione restituisce false

A questo punto, probabilmente avete capito l'essenza di ciò che sta accadendo.

Niente di meglio di un po' di contesto per chiarire...

La domanda rimane comunque: come si fa a dire a un componente quale API GraphQL interrogare? Utilizzando il contesto della query. Ecco un esempio:

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

Nota: sto usando react-apollo-hooks' useQuery per eseguire la query, ma funziona esattamente allo stesso modo usando il componente Query se non si usano gli hook di react.

Quindi, il punto dell'esempio è che possiamo impostare il valore del contesto su qualsiasi cosa vogliamo nella nostra query e poi sul sito della rete (nel nostro metodo ApolloLink in init-apollo.js). Basta assicurarsi di usare gli stessi valori in entrambi i posti.

A me piace impostare il collegamento del mio client Apollo come segue:

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

Si noti che il "primo collegamento" viene mostrato per secondo, il che lo rende di fatto l'origine dati predefinita da usare, se il valore del contesto impostato nella query non corrisponde al valore cercato dalla funzione split. Questo significa che si deve impostare il contesto solo quando si vuole usare il client "non predefinito". Questo significa che dobbiamo impostare il contesto solo quando vogliamo usare il nostro client "non predefinito".

Aggiornamento di with-apollo-client per avere esportazioni HTML completamente renderizzate

Ok, quindi siamo in dirittura d'arrivo: tutto quello che dobbiamo fare è modificare il nostro with-apollo-client.js in modo che corrisponda a quanto segue:

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

Si noti che le uniche modifiche/aggiunte sono le due importazioni finali (getMarkupFromTree & renderToString) e l'aggiunta della sezione getMarkupFromTree, che ci consente di avere esportazioni HTML statiche completamente renderizzate da Next.js.

Ma cosa succede se ho bisogno di più di due fonti di dati nella mia applicazione?

Beh, è strano che lo chiediate, perché il trucco di cui sopra (la sezione .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.

Questo secondo collegamento può essere un altro ApolloLink! Come si può vedere di seguito, si può continuare a concatenare i collegamenti fino a quando non si è verificato il contesto di tutte le possibili fonti di dati:

// 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: ma che dire delle query multiple?

Se ci si connette a più client, è probabile che da qualche parte nel progetto sia necessario eseguire due query nello stesso componente. Certo, si potrebbe importare compose da react-apollo nel nostro progetto, ma in questo caso non ce n'è davvero bisogno (source). Possiamo semplicemente creare il nostro hook (se si utilizzano gli hook di React). Il risultato è qualcosa di simile a:

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;

Un consiglio a @ThomasK33 per il suo aiuto nel risolvere questo problema.

Bonus #2: Che dire della mutazione su più fonti?

Al momento dell'ultimo aggiornamento (e aggiornerò questo post quando la situazione cambierà), la risposta sembra essere "dipende".

Se si usa un HOC <Mutation>, si può facilmente passare il contesto all'operazione (docs)) e selezionare la sorgente dei dati in modo simile a quanto fatto sopra.

Tuttavia, se si vuole passare il contesto a useMutation dalla libreria non ufficiale react-apollo-hooks, al momento non si è fortunati. La buona notizia è che la libreria ufficiale Apollo React Hooks è attualmente in beta e sembra certo che includerà il contesto come proprietà nella sua versione di useMutation, quindi è probabile che una soluzione sia dietro l'angolo.

Paul Grieselhuber

Paul Grieselhuber

Founder, President

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

Commenti

    Prenotate una telefonata di scoperta con i nostri esperti di prodotto.

    Il nostro team di esperti di applicazioni web e mobili sarà lieto di discutere con voi il vostro prossimo progetto.

    Prenotate una chiamata con noi 👋