question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

SSR (for Next static HTML export) with hooks

See original GitHub issue

In my project I have the following in place which forces me to not use Query, and instead in to a combination of ApolloConsumer + hooks in my components, which seems to not allow for fully rendered static HTML exports from next.

Why? Because I have two GraphQL data sources and need to be able to specify the client in the component, and Query doesn’t take this parameter.

What follows is my code from the following files, in this order:

  1. init-apollo.js
  2. with-apollo-client.js
  3. _app.js
  4. Post.js
  1. init-apollo.js
import getConfig from "next/config";
import ApolloClient from "apollo-boost";
import { InMemoryCache } from "apollo-cache-inmemory";
import { IntrospectionFragmentMatcher } from "apollo-cache-inmemory";
import { endpoint } from "../config";
import introspectionQueryResultData from "../fragmentTypes.json";

import fetch from "isomorphic-unfetch";

let apolloClient = null;

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

const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const fragmentMatcher = new IntrospectionFragmentMatcher({
  introspectionQueryResultData
});

const { WP_TOKEN } = publicRuntimeConfig;
function create(initialState) {
  const authHeaders = {
    authorization: WP_TOKEN ? `Bearer ${WP_TOKEN}` : ""
  };

  const wpClient = new ApolloClient({
    uri: process.env.NODE_ENV === "development" ? endpoint : endpoint,
    cache: new InMemoryCache({ fragmentMatcher }).restore(initialState || {}),
    request: operation => {
      operation.setContext({
        // fetchOptions: {
        //   credentials: "include"
        // },
        authHeaders
      });
    }
  });
  return wpClient;
}

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

  1. 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} />;
    }
  };
};
  1. _app.js
import App, { Container } from "next/app";
import Layout from "../components/Layout";
import { ApolloProvider } from "react-apollo";
import GlobalState from "../context/GlobalState";
import ApolloClient from "apollo-boost";
import { InMemoryCache } from "apollo-cache-inmemory";
import { secondEndpoint, secondApiToken } from "../config";
import withApolloClient from "../lib/with-apollo-client";

const createSecondClient = () => {
  const headers = {
    "Access-Token": secondApiToken
  };

  const client = new ApolloClient({
    uri: secondEndpoint,
    cache: new InMemoryCache(),
    fetchOptions: {
      credentials: "include"
    },
    request: operation => {
      operation.setContext({
        headers
      });
    }
  });
  return client;
};

class MyApp extends App {
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};
    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }
    pageProps.query = ctx.query;
    return { pageProps };
  }

  render() {
    const { Component, apolloClient, pageProps } = this.props;

    const wpClient = apolloClient;
    const secondClient = createSecondClient();

    return (
      <Container>
        <ApolloProvider client={{ wpClient, secondClient }}>
          <GlobalState>
            <Layout>
              <Component {...pageProps} />
            </Layout>
          </GlobalState>
        </ApolloProvider>
      </Container>
    );
  }
}

export default withApolloClient(MyApp);

  1. Post.js
const SinglePost = props => {
  let client;

  const [post, setPost] = useState({});
  const [thisPostId, setPostId] = useState(null);
  const [ctx, setContext] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  async function getPost() {
    console.log("the client", client);
    const response = await client.wpClient.query({
      query: GET_POST,
      variables: {
        uri: props.uri
      }
    });
    const {
      data: { postBy },
      error
    } = response;

    if (error) {
      setLoading({ message: "There was an error fetching the article" });
      setError(error);
    }
    setLoading(false);
    setPostId(postBy.postId);
    setPost(postBy);
  }

  useEffect(() => {
    getPost();
  }, post);

  return (
    <AppContext.Consumer>
      {context => {
        if (ctx === null) {
          setContext(context);
        }
        const { showGlobalCommentForm } = context;

        return (
          // We wrap this Query in an ApolloConsumer so that we have 'client' to pass on to the LoadComments component
          <ApolloConsumer>
            {c => {
              client = c;

So, the end result is that whereas previously when using the Query component, with only one client, when I would run next export and get a fully rendered static HTML version of the page, now I have to wait for the fetch and render.

I have read the documentation on the front page of this project for SSR, but I can’t figure out how to include the call for getMarkupFromTree in this structure, and if I could, if that would do the trick.

Thanks for any advice.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:9

github_iconTop GitHub Comments

1reaction
ThomasK33commented, Apr 23, 2019

@paulisloud you forgot the “getMarkupFromTree” and “renderToString” import in your blog post (in the final with-apollo-client.js file).

1reaction
paulisloudcommented, Apr 23, 2019

In case anyone comes across this in the future, here is a write-up of the method I ended up using: https://www.loudnoises.us/next-js-two-apollo-clients-two-graphql-data-sources-the-easy-way/

Read more comments on GitHub >

github_iconTop Results From Across the Web

Advanced Features: Static HTML Export | Next.js
Static Export. next export allows you to export your Next.js application to static HTML, which can be run standalone without the need of...
Read more >
Exporting a JSS Next.js app to static HTML files with next export
This walkthrough describes the necessary steps for exporting a JSS Next.js application to static HTML files using the Next.js command next ...
Read more >
Using SWR React Hooks With Next.js' Incremental Static ...
Our helpers file will export a function ( getTaxiData ) that will fetch the data from the external API, and then return it...
Read more >
How do I take advantage of Next.js server-side rendering ...
I tried to test the SSR/CSR behaviour by creating a site with some components with/without React hooks in them, and opening it in...
Read more >
Using Non-SSR Friendly Components with Next.js
This makes it possible to serve dynamic components as static HTML markup. SSR can be beneficial for search engine optimization (SEO) whenever indexing...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found