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で問題ない。