next.js + react-apollo-hooks で SSR する

公式の examples にないものを実装したのでメモ。

render-props ベースの https://github.com/zeit/next.js/tree/canary/examples/with-apollo では react-apollo-hooks と作りが違うので https://github.com/trojanowski/react-apollo-hooks の SSR の例を見ながら実装した。

lib/initApolloClient.js は一緒

```ts:lib/initApolloClient.ts
import {
  ApolloClient,
  InMemoryCache,
  HttpLink,
  NormalizedCacheObject
} from "apollo-boost";
import fetch from "isomorphic-unfetch";

let apolloClient: ApolloClient<any> | null = null;

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

function createApolloClient(initialState: NormalizedCacheObject) {
  // 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)
      uri: "http://localhost:3333/graphql",
      credentials: "same-origin" // Additional fetch() options like `credentials` or `headers`
    }),
    cache: new InMemoryCache().restore(initialState)
  });
}

export default function initApollo(initialState: any = {}) {
  // 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 createApolloClient(initialState);
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = createApolloClient(initialState);
  }

  return apolloClient;
}
```

apollo-client のキャッシュを組み立てないといけない。なので getDataFromTree の代わりに、`react-apollo-hooks/getMarkupFromTree` する。

```ts:lib/withApolloClient.ts
import React from "react";
import initApolloClient from "./initApolloClient";
import Head from "next/head";
import { renderToString } from "react-dom/server";
import { NextAppContext } from "next/app";
import { ApolloClient } from "apollo-boost";
import { getMarkupFromTree } from "react-apollo-hooks";

export default (
  App: React.ComponentType<any> & { getInitialProps?: Function }
) => {
  return class Apollo extends React.Component {
    static displayName = "withApollo(App)";
    apolloClient: ApolloClient<any>;
    static async getInitialProps(ctx: NextAppContext) {
      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 = initApolloClient();
      if (!process.browser) {
        try {
          // Run all GraphQL queries
          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: any) {
      super(props);
      this.apolloClient = initApolloClient(props.apolloState);
    }

    render() {
      return <App {...this.props} apolloClient={this.apolloClient} />;
    }
  };
};
```

あとは pages/_app.js に Provider を仕込む

```tsx
import React from "react";
import App, { Container } from "next/app";
import { createGlobalStyle } from "styled-components";
import withApolloClient from "../lib/withApolloClient";
import { ApolloClient } from "apollo-boost";
import { ApolloProvider } from "react-apollo-hooks";

class ApolloApp extends App<{ apolloClient: ApolloClient<any> }> {
  render() {
    const { Component, pageProps, apolloClient } = this.props;
    return (
      <Container>
        <ApolloProvider client={apolloClient}>
          <Component {...pageProps} />
        </ApolloProvider>
      </Container>
    )
  }
}

export default withApolloClient(ApolloApp);
```

## 使う

graphql は自分で実装してください。

```tsx:pages/apollo-ssr-test.tsx
import React from "react";
import gql from "graphql-tag";
import { useQuery } from "react-apollo-hooks";

const GET_USERS = gql`
  query {
    users {
      id
      name
    }
  }
`;

export default function Apollo() {
  const { loading, data } = useQuery(GET_USERS);

  return (
    <div>
      {!loading && (
        <ul>
          {data.users.map((u: { id: string; name: string }) => {
            return <li key={u.id}>{u.name}</li>;
          })}
        </ul>
      )}
    </div>
  )
}
```