LoudNoises logo

Next.js + Multiple Apollo Clients & GraphQL Sources

Cover Image for Next.js + Multiple Apollo Clients & GraphQL Sources
Posted: under GraphQL,Next.js,React
Reading time: 11 min read

This blog post is over two years old and the underlying methods may have changed. We maintain it publicly only for archival purposes, and in case you have a legacy code need.

Next.js + GraphQL turns out to be a pretty stellar way to build both small & large-scale static websites, especially if you make use of Next.js' static HTML exports (see my post on WPGraphQL for a how-to on this using GraphQL).

One of the questions that many developers seem to have, and which takes an astonishing amount of Googling, research and testing to sort out is building one of these sites with two GraphQL data sources.

A bit of background & boilerplate

To set the stage for the solution, here is why this ends up being a little bit of a challenge. The basic setup for an Apollo client in Next.js source involves three files:

  • Our /pages/_app.js component
  • /lib/init-apollo.js
  • /lib/with-apollo-client.js

First, lets take a look at _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);

The gist here is that we import withApolloClient and wrap the rest of our app in that component, passing the apolloClient that comes along from props as a result. This gives our child components access to the data coming from the GraphQL source that Apollo is connected to. Onward.

In init-apollo.js, we create the Apollo client and tell it where to connect:

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

Pretty straightforward what is going on here, but keep an eye on that link line, we'll be coming back to that...

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

Here we return two different versions of our HOC App component (which later wraps the child components in _app.js): the first is for SSR mode, the second is for everything else.

Apollo expects one client

You've probably noticed the implicit assumption in all of the logic above that there is precisely one client.

The problem is I want multiple data sources, and I want to query them in the style that the libraries I am using in my project intend. And I don't want to hack something together that will destroy my ability to benefit from all of Apollo's great features, etc.

One option that came up was schema stitching, but it just seemed like way more than I wanted to get into just to be able to query two or more clients.

After having lots of discussions, doing lots of research, and refactoring more times than I'd like to admit, I realized that it comes down the what you really want to have "multiple" of.

The solution

So what is the answer here? Do I want multiple clients? Multiple providers?

Nope, you're way off. And probably because I misled you with the title. Or perhaps because the Angular Apollo library has this option, and it is in fact done with multiple clients.

But we're in the React world, and the answer is: multiple links in your one Apollo client.

Luckily to get there we only have to make minor changes to two of our three files.

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 of the Apollo Client to the rescue

Note the changes made:

  • We factored out our first link, for readability, since we will be creating a second one (note that you can pass any options you need to Apollo link modules here)
  • We imported ApolloLink, which has as an available method .split(). This method takes at least two parameters (though it doesn't really make sense to pass less than three):
  • The first is a function to decide which link to use
  • The second is the link to use if this function returns true
  • The third is the link to use if the function returns false

At this point, you probably get the essence of what's going on here.

Nothing like a bit of context to clarify...

The question remains though, how do we tell a component which GraphQL API to query? By using the context of the query. Here is an example:

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

Note: I am using react-apollo-hooks' useQuery to run the query, but works exactly the same using the Query component if you are not using react hooks.

So, the point of that example is that we can set the value of context to anything we want in our query, and then on the network site (in our ApolloLink method in init-apollo.js). Just make sure you use the same values in both places.

What I like to do is set up my Apollo client's link as follows:

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

Note that the "firstLink" is shown second, which actually makes it the default data source to use, if the context value set in the query does not match the value the split function looks for. This means that we only have to set context when we want to use our "non-default" client.

Updating with-apollo-client to give us fully rendered HTML exports

Ok, so entering the home stretch here, all we need to do it edit our with-apollo-client.js to match the following:

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

Note the only changes / additions are the two imports final imports (getMarkupFromTree & renderToString), and the addition of the getMarkupFromTree section, which allows us to have fully-rendered static HTML exports from Next.js.

But what if I need more than two data sources in my application?

Well, funny you should ask, because the trick above (the .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.

This second link can be another ApolloLink! As you can see below, we can just keep chaining links together until we've checked context on all possible data sources:

// 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: but what about multiple queries?

If you are connecting to multiple clients, chances are that somewhere in your project you will need to run two queries in the same component. Sure, we could import compose from react-apollo in to our project, but there's really no need in this case (source). We can simply make our own hook (if you are using React hooks). This ends up looking something like:

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;

Hat tip to @ThomasK33 for his help on sussing this out.

Bonus #2: What about mutating on multiple sources?

As of the most recent update here (and I will update this post as the situation changes), the answer seems to be "it depends".

If you are using a <Mutation> HOC, you can easily pass context to the operation (docs), and select your data source in a similar fashion to what we did above.

However, if you want to pass context to useMutation from the unofficial react-apollo-hooks library, you appear to be out of luck at present. The good news is that the official Apollo React Hooks library is currently in beta, and it seems a certainty that they would include context as a property in their version of useMutation, so a solution here is likely around the corner.

Let's talk. We can help grow your business.

Your company is already off to a running start and you are ready for some serious growth. You're seeking the right web technology partner to propel your business forward. You've come to the right place.


Tell us a bit about your business.

Comments