LoginSignup
7
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-20

個人的に開発していてめっちゃ苦戦したのでメモ。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 が全く更新されていなくて使えないので、ソース見ながらほぼパクって getDataFromTreegetMarkupFromTreeに変えました

// 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 も参考にしました

以下解決コード

// 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を使う

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

終わり

7
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7