【Shopify/Relay/NextJs/TypeScript】shopifyのcustomerAccessTokenを初回アクセス時にrelay-nextjsのwithRelayで取れずnextのGetServerSidePropsを使った話

# はじめに
最近業務でshopify/Relay/NextJsでECサイトをフロントエンド開発をする機会があり、shopifyの「customerオブジェクトにアクセスするためのcustomerAccessToken」の初回取得ができずハマった箇所があったためその備忘録です。

前提として、GraphQLライブラリはRelayを使用しており、customerAccessTokenはライブラリの[nookies](https://github.com/maticzav/nookies)でhttp only管理し、サーバーからしか参照・取得できない設計にしています。
※この辺の実装は今回は割愛。



### 結論：relayで初回ページアクセス時にtokenが必要な場合はrelay-nextjsのserverSidePropsではなくnextのGetServerSidePropsを使う

```tsx:初回リダイレクト時からcustomerAccessTokenトークン取得可
.
.

import type { NextPage, GetServerSideProps } from 'next';
import React, { Suspense, useEffect, useState } from 'react';
import { useQueryLoader } from 'react-relay';
import { createServerEnvironment } from '../lib/serverEnvironment';

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { parseCookies } = await import('nookies'); 
  const { token } = parseCookies(context); // サーバーから取得したcontextからtokenをparse
  if (!token) {
    return {
      redirect: {
        permanent: false,
        destination: '/',
      },
    };
  }
  return {
    props: {
      customerAccessToken: token ?? '', // parse済みのtokenをpropsでDemoPage👇に渡す
    },
  };
};

type Props = {
  customerAccessToken: string;
};
// getServerSidePropsからPropsのcustomerAccessTokenでtokenを受け取る
const DemoPage: NextPage<Props> = ({ customerAccessToken }: Props) => {
  const [, loadCustomerQuery] =
    useQueryLoader<CustomerQueryType>(customerQuery);

  const [customer, setCustomerOrder] =
    useState<CustomerQueryResponse>();

  // useLazyLoadQueryではロードできないのでuseQueryLoader後にfetchQuery関数でquery実行
  useEffect(() => {
     // queryロードをしないとpropsで子コンポーネントに渡った先のuseFragmentでこける
    loadCustomerQuery({ customerAccessToken });
    // queryのfetch関数はgetServerProps内で叩くと、データは読まれるが「The server could not finish this Suspense boundary, likely due to an error during server rendering.Switched to client rendering」が吐かれるのでここで実行
    fetchCustomerQuery({
      environment: createServerEnvironment(),
      customerAccessToken,
    }).then((result) => setCustomer(result));
  // stateにデータをセットして子コンポーネントにpropsで渡す
  }, [customerAccessToken, loadCustomerQuery]);

  if (!isLoggedIn || !customer) {
    return <Loading />;
  }
  return (
    <Suspense fallback={<Loading />}>
      <ChildComponent customer={customer} />
    </Suspense>
  );
};
export default DemoPage;


```
fetchCustomerQueryでqueryを実行するだけで子コンポーネントで行うuseFragment処理のタイミングに間に合わないので、useEffectでqueryロードしてそのまま、fetchCustomerQuery関数でquery実行したレスポンスを子コンポーネントに渡す。これにより最短でqueryを子コンポーネントに渡すことが出来、子側でuseFragmentも問題なく展開される。

```ts:customerのGraphQLクエリ定義＆fetchCustomerQuery関数

import { Environment, fetchQuery, graphql } from 'react-relay';
import type {
  customerQuery as CustomerQueryType,
  customerQuery$data as CustomerQueryResponse,
} from './__generated__/customerQuery.graphql';

export const customerQuery = graphql`
  query customerQuery($customerAccessToken: String!) {
    customer(customerAccessToken: $customerAccessToken) {
      firstName
      lastName
      defaultAddress {
        address1
        address2
        lastName
        firstName
        province
        zip
        city
        id
      }
      orders(first: 250) {
        edges {
          node {
            ...orderFragment
          }
        }
      }
    }
  }
`;

type FetchGetCustomerQuery = {
  environment: Environment;
  customerAccessToken: string;
};
export const fetchCustomerQuery = ({
  environment,
  customerAccessToken,
}: FetchCustomerQuery): Promise<
  CustomerResponse | undefined
> =>
  fetchQuery<CustomerQueryType>(
    environment,
    CustomerQuery,
    {
      customerAccessToken,
    },
  ).toPromise();

```

### 別のケース：relay-nextjsのserverSidePropsでトークンを取った場合
使用GraphQLライブラリがRelayのため、はじめ[relay-nextjs](https://github.com/RevereCRE/relay-nextjs)のwithRelayのserverSidePorps・variablesFromContextを使ってトークン参照・取得しようとしたところ、ページ初回アクセス（リダイレクト時）にcustomerAccessTokenがundefinedになり、ページリロードをするまでqueryデータの参照ができない状況になりました。
[relay-nextjs開発者のコメント](https://github.com/RevereCRE/relay-nextjs/issues/37#issuecomment-983822477)を読んだところ、そういった設計になっているようですね。

```tsx:初回リダイレクト時customerAccessTokenはundefinedリロードするとトークン取得可
import { withRelay, RelayProps } from 'relay-nextjs';
import type {
  NextPageContext,
  NextPage,
  Redirect,
} from 'next';
import { GraphQLTaggedNode, useLazyLoadQuery, Variables } from 'react-relay';
import { customerQuery } from '../../lib/graphql/customerQuery';
import { customerQuery as CustomerQueryType } from '../../lib/graphql/__generated__/customerQuery.graphql';

type Props = {
  redirect?: Redirect;
  customerAccessToken: string;
};

type CompositeProps = RelayProps<Props, GetCustomerQueryType>;

const DemoPage: NextPage<CompositeProps, Props> = ({
  customerAccessToken,
}: CompositeProps) => {
const [
    customerInitialPreloadedQuery,
    setCustomerInitialPreloadedQuery,
  ] = useState<AnyPreloadedQuery | null>();

  const relayProps = getRelayProps(
    customerQuery,
    customerInitialPreloadedQuery as AnyPreloadedQuery | null,
  );
  const relayPropsCustomerAccessToken = relayProps.preloadedQuery?.variables
    .customerAccessToken as string;

  const accessToken =
    relayPropsCustomerAccessToken !== undefined
      ? customerAccessToken
      : customerAccessTokenState;


  const customer = useLazyLoadQuery<CustomerQueryType>(
    customerQuery,
    {customerAccessToken: accessToken},
  );
  useEffect(() => {
    const initialPreloadedQuery = getInitialPreloadedQuery({
      createClientEnvironment: () => getClientEnvironment()!,
    });
    setCustomerInitialPreloadedQuery(initialPreloadedQuery);
  }, []);

  if (!isLoggedIn) {
    return <Loading />;
  }
  return (
    <Suspense fallback={<Loading />}>
      <ChildComponent customer={customer} />
    </Suspense>
  );
};
export default withRelay(DemoPage, getCustomerQuery, {
  fallback: <Loading />,

  variablesFromContext: (ctx: NextRouter | NextPageContext) => {
    const { token } = parseCookies(ctx as NextPageContext);
    return {
      customerAccessToken: token ?? '',
    };
  },
  serverSideProps: async (ctx: NextPageContext | NextRouter) =>
    new Promise<Props>((resolve) => {
      const { token } = parseCookies(ctx as NextPageContext);
      if (token === undefined) {
        resolve({
          redirect: {
            permanent: false,
            destination: '/',
          },
          customerAccessToken: token ?? '',
        });
      } else {
        resolve({
          customerAccessToken: token ?? '',
        });
      }
    }),
  createClientEnvironment: () => getClientEnvironment()!,
  createServerEnvironment: async () => {
    const { createServerEnvironment } = await import(
      '../lib/server/serverEnvironment'
    );
    return createServerEnvironment();
  },
});

```
以上です。