はじめに
最近業務でshopify/Relay/NextJsでECサイトをフロントエンド開発をする機会があり、shopifyの「customerオブジェクトにアクセスするためのcustomerAccessToken」の初回取得ができずハマった箇所があったためその備忘録です。
前提として、GraphQLライブラリはRelayを使用しており、customerAccessTokenはライブラリのnookiesでhttp only管理し、サーバーからしか参照・取得できない設計にしています。
※この辺の実装は今回は割愛。
結論:relayで初回ページアクセス時にtokenが必要な場合はrelay-nextjsのserverSidePropsではなくnextのGetServerSidePropsを使う
.
.
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も問題なく展開される。
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のwithRelayのserverSidePorps・variablesFromContextを使ってトークン参照・取得しようとしたところ、ページ初回アクセス(リダイレクト時)にcustomerAccessTokenがundefinedになり、ページリロードをするまでqueryデータの参照ができない状況になりました。
relay-nextjs開発者のコメントを読んだところ、そういった設計になっているようですね。
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();
},
});
以上です。