Next.js_3 (Hasura GraphQLサーバーとクライアントサイドの連携編)
ポイントは、サーバーサイドの処理と、フロントエンドだけの処理を、きちんと切り分けて構築すること。
公式ライブラリを多用する。
https://github.com/vercel/next.js/blob/canary/examples/with-apollo/lib/apolloClient.js
■9. ライブラリのファイルを作成
9-1. ルートディレクトリにlib ディレクトリを作成し、その中にapolloClient.tsファイルを作成
9-2. 必要なインポート
import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject, } from '@apollo/client' import 'cross-fetch/polyfill'
公式ライブラリから、APOLLO_STATEを、そのままコピペ。
import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject, } from '@apollo/client' import 'cross-fetch/polyfill' export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'
apolloClientの変数を定義。これも決まった形であるので、そのままコピペ。
公式ライブラリから、APOLLO_STATEを、そのままコピペ。
apolloClientのお決まりのデータ型、仕様である。
import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject, } from '@apollo/client' import 'cross-fetch/polyfill' export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__' let apolloClient: ApolloClient<NormalizedCacheObject> | undefined
関数を作成。まずは基本形。
~省略~ export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__' let apolloClient: ApolloClient<NormalizedCacheObject> | undefined const createApolloClient = () => { return new ApolloClient({ ssrMode: typeof window === 'undefined', link: new HttpLink({ uri: '', }), cache: new InMemoryCache(), }) }
ssrMode→サーバーサイドレンダリングモード。window はブラウザで実行している、という意味になり、ブラウザじゃない場合、つまりサーバーサイドで走っている場合という解釈になる。
link→HasuraのエンドポイントのURIを指定する。なので、HasuraのGraphQLのAPIからURIをコピペする
~省略~ export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__' let apolloClient: ApolloClient<NormalizedCacheObject> | undefined const createApolloClient = () => { return new ApolloClient({ ssrMode: typeof window === 'undefined', link: new HttpLink({ uri: 'https://closing-chamois-44.hasura.app/v1/graphql', }), cache: new InMemoryCache(), }) }
次に、イニシャライズアポロという関数を作る。
export const initializeApollo = (initialState = null) => { const _apolloClient = apolloClient ?? createApolloClient() // For SSG and SSR always create a new Apollo Client if (typeof window === 'undefined') return _apolloClient // Create the Apollo Client once in the client if (!apolloClient) apolloClient = _apolloClient return _apolloClient }
// For SSG and SSR always create a new Apollo Client
→サーバーサイドの場合になる。常に新しいapolloClientを作成する、の意味になる。
// Create the Apollo Client once in the client
→クライアントサイドの場合になる。最初の1回だけapolloClientを作成する、の意味になる。
apolloClient ?? createApolloClient() の意味については(「??」の意味)、
左辺がnullまたはundefinedの場合は右辺の処理が実行されて、_apolloClientに代入される。
左辺に何らかの値が入っている場合は、そのまま左辺の値が_apolloClientに代入される。
全体の流れを見ると、SSGやSSRはサーバーサイドで実行されるので、最初にinitializeApollo をしたときは、apolloClient は存在しないので、createApolloClient()が実行されて、new ApolloClientが生成される。
で、生成されたApolloClientが_apolloClient に代入される。
そして、SSGの場合は、window がundefined(windowはブラウザを意味しているので、それがundefinedということは、サーバーサイドの処理に入っている)なので、即座にreturnで今新規で作成した_apolloClientをreturnで返すようにしている。
今のSSGやSSRの処理の流れの中では、全体のapolloClientの変数の中には何も代入するという処理が無いので、次のinitializeApollo が実行された場合も「??」の評価のところで、左辺のapolloClient がnullまたはundefinedになるので、毎回右辺のcreateApolloClient()が実行されて、新しいcreateApolloClientが実行されて、生成される形になる。
この結果、「For SSG and SSR always create a new Apollo Client」の要求が満たされる形になる。
一方、クライアントサイドの流れでは、一度最初にinitializeApollo のところに入ってきたときは、最初はapolloClient が空なので、右辺のcreateApolloClient()が実行されて、新しくcreateApolloClientがクライアントサイドで作られる。
そして、作られたapolloClient が、_apolloClient に入ってくる。
そして、if (typeof window === 'undefined')の評価式は、クライアントサイドはfalseなので、スキップされて、if (!apolloClient)が評価される。まだ1回目なので、全体のapolloClientは空なので、if (!apolloClient)がtrueになって、全体のapolloClientに今新規で作成した_apolloClient に入っているクライアントが全体のapolloClientに代入されてlet apolloClientがクライアントで埋まることになる。
そして、return では _apolloClientを返すことになる。
そして、クライアントサイドで、再度2回目以降、initializeApollo の処理に入ってきた場合は、全体のlet apolloClient:が既に1回目の処理で埋まっているので、 apolloClient ?? createApolloClient()の左辺が既に埋まっているので、_apolloClientにはapolloClient が代入される。
そしてif (typeof window === 'undefined')はサーバーサイドなのでスキップされて、2回目以降はクライアントサイドは既に値が入っているので、if (!apolloClient) はfalseになって、 if (!apolloClient) apolloClient = apolloClientの処理もスキップされて、apolloClientで代入された値がそのままreturnされるので、既存の、初回に作成したapolloClientが毎回再利用されていく形になる。
ここが、Next.jsで、サーバーサイドとクライアントサイドを切り分ける、という一番大事なところになる。
■10. ライブラリをつかって、apolloClientを作成する
pages/_app.tsxを開く。
ApolloProviderとapolloClientをインポート。
import { AppProps } from 'next/app' import { ApolloProvider } from '@apollo/client' import { initializeApollo } from '../lib/apolloClient' import '../styles/globals.css' function MyApp({ Component, pageProps }: AppProps) { return <Component {...pageProps} /> } export default MyApp
initializeApolloを実行して、実際にクライアントを作成していく。
import { AppProps } from 'next/app' import { ApolloProvider } from '@apollo/client' import { initializeApollo } from '../lib/apolloClient' import '../styles/globals.css' function MyApp({ Component, pageProps }: AppProps) { const client = initializeApollo() return <Component {...pageProps} /> } export default MyApp
このconst clientが、コンポーネントの全てで使えるように、ApolloProvider でこのコンポーネントを囲っていく。
そのときに、propsでclientを渡すことで、プロジェクト内のあらゆるコンポーネントで、このapoloClinetが使えるようになる。
import { AppProps } from 'next/app' import { ApolloProvider } from '@apollo/client' import { initializeApollo } from '../lib/apolloClient' import '../styles/globals.css' function MyApp({ Component, pageProps }: AppProps) { const client = initializeApollo() return ( <ApolloProvider client={client}> <Component {...pageProps} /> </ApolloProvider> ) } export default MyApp
■11. 実際に、このクライアントを使って、Hasuraにクエリを投げて、ユーザーの一覧のデータを取得する。
pages/にhasura-main.tsxを作成。
hasura-main.tsxに必要なモジュールをインポートと基本形。
import { VFC } from 'react' import Link from 'next/link' import { useQuery } from '@apollo/client' import { GET_USERS } from '../queries/queries' import { GetUsersQuery } from '../types/generated/graphql' import { Layout } from '../components/Layout' const FetchMain: VFC = () => { return( ) }
なお、自動生成された../types/generated/graphqのクエリは、生成規則があり、今回生成元の../queries/queriesにあるGET_USERS のクエリ名である「GetUsers」の後ろに「Query」が付く、という規則になっている。なので、そのように書けば、探す手間は省ける。
ファンクショナルコンポーネントでimportしたGET_USERSを実行したいので、useQueryを定義していく。
import { VFC } from 'react' import Link from 'next/link' import { useQuery } from '@apollo/client' import { GET_USERS } from '../queries/queries' import { GetUsersQuery } from '../types/generated/graphql' import { Layout } from '../components/Layout' const FetchMain: VFC = () => { const { data, error } = useQuery<GetUsersQuery>(GET_USERS) return( ) }
useQueryの引数のところに、実行したいクエリのコマンドを指定して、そしてジェネリックス(
そして、{ data, error } の返り値として、取得されたデータ、通信中にエラーが発生した場合はerrorのところがtrueになる。あとは
const { data, error, loading }
というものもあり、通信中はloadingがtrueになる、という形になる。
まず、エラーが発生した場合の処理を書いていく。
~省略~ const FetchMain: VFC = () => { const { data, error } = useQuery<GetUsersQuery>(GET_USERS) if (error) return ( <Layout title="Hasura fetchPolicy"> <p>Error: {error.message}</p> </Layout> ) return( ) }
errorがtrueになるとtrueになり、実行される。
errorにならなかった場合の処理を書く。
~省略~ const FetchMain: VFC = () => { const { data, error } = useQuery<GetUsersQuery>(GET_USERS) if (error) return ( <Layout title="Hasura fetchPolicy"> <p>Error: {error.message}</p> </Layout> ) return ( <Layout title="Hasura fetchPolicy"> <p className="mb-6 font-bold">Hasura main page</p> {console.log(data)} {data?.users.map((user) => { return ( <p className="my-1" key={user.id}> {user.name} </p> ) })} <Link href="/hasura-sub"> <a className="mt-6">Next</a> </Link> </Layout> ) } export default FetchMain
ではyarn devで検証
yarn dev
■11. 存在するキャッシュに、@clientを使って、アクセスする。
pages/にhasura-sub.tsxを作成
まずは基本形。
import { VFC } from 'react' import Link from 'next/link' import { useQuery } from '@apollo/client' import { GET_USERS_LOCAL, GET_USERS } from '../queries/queries' import { GetUsersQuery } from '../types/generated/graphql' import { Layout } from '../components/Layout' const FetchSub: VFC = () => { const { data } = useQuery<GetUsersQuery>(GET_USERS_LOCAL) return ( ) } export default FetchSub
まずは、最初のやり方から。
import { VFC } from 'react' import Link from 'next/link' import { useQuery } from '@apollo/client' import { GET_USERS_LOCAL, GET_USERS } from '../queries/queries' import { GetUsersQuery } from '../types/generated/graphql' import { Layout } from '../components/Layout' const FetchSub: VFC = () => { const { data } = useQuery<GetUsersQuery>(GET_USERS) return ( <Layout title="Hasura fetchPolicy read cache"> <p className="mb-6 font-bold">Direct read out from cache</p> {data?.users.map((user) => { return ( <p className="my-1" key={user.id}> {user.name} </p> ) })} <Link href="/hasura-main"> <a className="mt-6">Back</a> </Link> </Layout> ) } export default FetchSub
useQuery は、オプションで「fetch policy」を設定することができ、何もオプションを設定しない場合は、キャッシュファーストというオプションが有効になっていて、キャッシュが存在する場合は常にそのキャッシュの値を見に行く、という処理になる。
テストのため、fetchPolicyを追加。
import { VFC } from 'react' import Link from 'next/link' import { useQuery } from '@apollo/client' import { GET_USERS_LOCAL, GET_USERS } from '../queries/queries' import { GetUsersQuery } from '../types/generated/graphql' import { Layout } from '../components/Layout' const FetchSub: VFC = () => { const { data } = useQuery<GetUsersQuery>(GET_USERS, { fetchPolicy: 'network-only' }) return ( <Layout title="Hasura fetchPolicy read cache"> <p className="mb-6 font-bold">Direct read out from cache</p> {data?.users.map((user) => { return ( <p className="my-1" key={user.id}> {user.name} </p> ) })} <Link href="/hasura-main"> <a className="mt-6">Back</a> </Link> </Layout> ) } export default FetchSub
このfetchPolicy: 'network-onlyは、useQueryが実行されるたびにGraphQLのサーバーに毎回アクセスして、最新のデータを取得して、そちらをキャッシュに格納してくれる形になる。
なお、fetchPolicyは4種類ある。
fetchPolicy: 'chache-and-network';
fetchPolicy: 'network-only';
fetchPolicy: 'chache-first';
fetchPolicy: 'no-chache';
利用する際は、9割方、chache-and-networkで問題ない。