Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSR (for Next static HTML export) with hooks #132

Closed
paulisloud opened this issue Apr 16, 2019 · 9 comments

Comments

Projects
None yet
2 participants
@paulisloud
Copy link

commented Apr 16, 2019

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.

@ThomasK33

This comment has been minimized.

Copy link

commented Apr 21, 2019

Update your with-apollo-client.js file to include:

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

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

This has also been pointed out by another person in #52 (comment)

@paulisloud

This comment has been minimized.

Copy link
Author

commented Apr 21, 2019

Unfortunately, even with that change in place, my queries are still running after the page loads in the static HTML export, instead of having been fully rendered via the export process.

@ThomasK33

This comment has been minimized.

Copy link

commented Apr 21, 2019

Your scenario (not fully rendered) only happens once one or multiple graphql errors occurred during „getDataFromTree“ / „getMarkupFromTree“.

Asides from that, I quickly just glanced over your code and you forgot to import the ApolloProvider of this module. ( See )

Add it to your existing providers in _app.js and add the snipet from above and you should be good to go.

@paulisloud

This comment has been minimized.

Copy link
Author

commented Apr 21, 2019

Thanks for the feedback, but still not getting a rendered export.

I have checked for any graphql errors (there are none that I see in the console on any page), and I have imported the module you mentioned, and still have the same result. Is there a way to enable logging of graphql renders during build so I can make sure no pages are returning errors? The catch statement in with-apollo-client never catches.

_app.js current stands as :

import App, { Container } from "next/app";
import Layout from "../components/Layout";
import { ApolloProvider } from "react-apollo";
import { ApolloProvider as ApolloHooksProvider } from "react-apollo-hooks";
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 }}>
          <ApolloHooksProvider client={{ wpClient, secondClient }}>
            <GlobalState>
              <Layout>
                <Component {...pageProps} />
              </Layout>
            </GlobalState>
          </ApolloHooksProvider>
        </ApolloProvider>
      </Container>
    );
  }
}

export default withApolloClient(MyApp);

and with-apollo-client.js stands as:

import React from "react";
import initApollo from "./init-apollo";
import Head from "next/head";
import { getDataFromTree } from "react-apollo";
import { renderToString } from "react-dom/server";
import { getMarkupFromTree } from "react-apollo-hooks";

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

@ThomasK33

This comment has been minimized.

Copy link

commented Apr 21, 2019

-<ApolloProvider client={{ wpClient, secondClient }}>
-          <ApolloHooksProvider client={{ wpClient, secondClient }}>
            <GlobalState>
              <Layout>
                <Component {...pageProps} />
              </Layout>
            </GlobalState>
          </ApolloHooksProvider>
        </ApolloProvider>

These two lines look fishy, more precisely that you pass an object containing two clients.

In the typings of this library and of apollo react one can see that only one apollo client is expected and not an object containing multiple.

If you want to utilize multiple clients, consider using a composite link with two http links, each pointing to a different endpoint.

@paulisloud

This comment has been minimized.

Copy link
Author

commented Apr 22, 2019

Yep, two clients was the initial point of my question:

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.

When I only had one client, I was getting static HTML renders no problem. This comment on this issue for react apollo was the inspiration for my going with the direction of putting two clients on the client object passed to ApolloProvider.

This setup does indeed function, and I can run queries like so:

const response = await client.wpClient.query({
   query: GET_POST,
   variables: {
     uri: props.uri
   }
});

vs client.query, as you would usually do with ApolloConsumer.

In any event, the composite link information you shared sent me down the right path, and was able to refactor using multiple links and useQuery. After that, I now get static HTML exports again.

Thanks for your help!

@paulisloud paulisloud closed this Apr 22, 2019

@paulisloud

This comment has been minimized.

Copy link
Author

commented 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/

@paulisloud paulisloud reopened this Apr 23, 2019

@paulisloud paulisloud closed this Apr 23, 2019

@ThomasK33

This comment has been minimized.

Copy link

commented Apr 23, 2019

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

@paulisloud

This comment has been minimized.

Copy link
Author

commented Apr 23, 2019

Good catch, many thanks for taking the time to go through the post. Updated and added a hat tip @ThomasK33.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.