Next.js + Multiple Apollo Clients & GraphQL Sources
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.
Comments