Next.js + Apollo Client で cookie を送れない件について

個人的に開発していてめっちゃ苦戦したのでメモ。SSRのやり方はわかっていることを前提に書きます

具体的に
リロード時（クライアントナビゲーションではなく）にサーバー側に cookie 情報が送られないという問題に遭遇しました。

前提条件
Apollo Client を使用して、初期ロード時にデータのキャッシュを作成している（SSR）

解決
とりあえず、`@apollo/react-hooks`を使ってるなら、`getDataFromTree`は使えません。どこで読んだかは覚えていませんが、`getDataFromTree`はdeprecatedらしく、代わりに `@apollo/react-ssr`がエキスポートする`getMarkupFromTree` を使えとこのとです（ https://github.com/apollographql/react-apollo/issues/3251 ）。

Apollo Clientのサーバー initialize めんどくさいんで書きたくなかったんですが、https://github.com/lfades/next-with-apollo が全く更新されていなくて使えないので、ソース見ながらほぼパクって `getDataFromTree`を`getMarkupFromTree`に変えました

```js
// withApollo.js

import React from "react";
import Head from "next/head";
import "isomorphic-unfetch";
import { renderToString } from "react-dom/server";
import { getMarkupFromTree } from "@apollo/react-ssr";

let _apolloClient = null;

// same implementation with next-with-apollo,
// just subtituting getDataFromTree -> getMarkupFromTree for hook support
const getClient = (clientFn, options = {}) => {
  return clientFn(options);
};

const initApollo = (clientFn, options) => {
  if (!clientFn) {
    throw new Error(
      "[withApollo] the first param is missing and is required to get the ApolloClient"
    );
  }

  if (!process.browser) {
    return getClient(clientFn, options);
  }

  if (!_apolloClient) {
    _apolloClient = getClient(clientFn, options);
  }

  return _apolloClient;
};

export default function withApollo(client) {
  return App => {
    return class WithApollo extends React.Component {
      constructor(props) {
        super(props);
        this.displayName = "withApollo(App)";
        this.apollo =
          props.apollo ||
          initApollo(client, {
            initialState: props.apolloState.data
          });
      }

      static async getInitialProps(appCtx) {
        const { AppTree, ctx, router } = appCtx;

        const headers = ctx.req ? ctx.req.headers : {};
        const apollo = initApollo(client, { ctx, headers });
        const apolloState = {};
        const getInitialProps = App.getInitialProps;

        let appProps = {
          pageProps: {}
        };

        if (getInitialProps) {
          ctx.apolloClient = apollo;
          appProps = await getInitialProps(appCtx);
        }

        if (ctx.res && (ctx.res.headersSent || ctx.res.finished)) {
          return {};
        }

        if (!process.browser) {
          try {
            await getMarkupFromTree({
              renderFunction: renderToString,
              tree: (
                <AppTree
                  {...appProps}
                  router={router}
                  apolloState={apolloState}
                  apollo={apollo}
                />
              )
            });
          } catch (e) {
            console.error("Error while running `getMarkupFromTree`", e);
          }

          Head.rewind();
          apolloState.data = apollo.cache.extract();
        }

        apollo.toJSON = () => {
          return null;
        };

        return {
          ...appProps,
          apolloState,
          apollo
        };
      }

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

```

ここまではいいのですが、SSR時に送りたいcookie情報をサーバーに送れないという問題にぶち当たりました。https://qiita.com/mizchi/items/0942be44dba68783a170  も参考にしました

以下解決コード

```js
// initApolloClient.js

import { ApolloClient, InMemoryCache, HttpLink } from "apollo-boost";
import fetch from "isomorphic-unfetch";
import withApollo from "./withApollo";
import { endpoint } from "../config";

let apolloClient = null;

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

function createApolloClient(options) {
  // このメソッドがSSR時にcookieをアペンドしてくれる
  const customFetch = headers => (uri, options) => {
    options.headers.cookie = headers.cookie;
    return fetch(uri, options);
  };

  // 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: process.env.NODE_ENV === "development" ? endpoint : endpoint,
      credentials: "include",
      // SSRの時だけ。
      fetch: !process.browser && customFetch(options.headers)
    }),
    cache: new InMemoryCache().restore(options.initialState || {})
  });
}

function initApollo(options) {
  // 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(options);
  }

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

  return apolloClient;
}

export default withApollo(initApollo);
```

あとは`_app.js`でHOCを使う

```js
import App from "next/app";
import Page from "../components/Page";
import { ApolloProvider } from "@apollo/react-hooks";
import initApolloClient from "../lib/initApolloClient";

class MyApp extends App {
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};
    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }
    // this exposes the query to the user
    pageProps.query = ctx.query;

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

    return (
      <ApolloProvider client={apollo}>
        <Page>
          <Component {...pageProps} />
        </Page>
      </ApolloProvider>
    );
  }
}

export default initApolloClient(MyApp);
```

終わり