Next.jsではクライアントサイドとサーバーサイドの処理を扱うことができるのでApolloClientを使う場合は処理の切り分けが重要になってきます。
サーバーサイドで実行される箇所(SSG、ISR:getStaticProps、getStaticPaths)
Next.jsは毎回Apollo Clientのインスタンスを生成しなさいと謳っています。ちゃんとこの部分を意識しないとNext.jsのSSGやISRが変な挙動になってしまったりするので意識して実装する必要があります。
クライアントで実行される箇所
最初に1回だけApollo Clientのインスタンスを生成すれば良いですよと言っています。
Apollo Clientの実装例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject, } from '@apollo/client' import 'cross-fetch/polyfill' let apolloClient: ApolloClient<NormalizedCacheObject> | undefined const createApolloClient = () => { return new ApolloClient({ ssrMode: typeof window === 'undefined', link: new HttpLink({ uri: "HasuraのURL", }), 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 } |
ssrMode: typeof window === 'undefined'
windowはブラウザで実行しているかという意味になります。なので、ブラウザじゃない(サーバーサイド)の場合にtrueが入ります。クライアントサイドで実行する場合はfalseが入ります。
cache: new InMemoryCache(),
こちらはお決まりの文法でApollo Clientが自動でcacheに値を格納してくれます。
initializeApollo関数
windowオブジェクトを利用して、クライアントサイドは最初の1回だけインスタンスを生成して、サーバーサイドの場合は呼び出すたびにApolliClientインスタンスを生成するというような関数内容になっています。
呼び出し方
以下のように「ApolloProvider」を使ってNext.jsの_app.tsxなどに渡してあげます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import '../styles/globals.css' import { AppProps } from 'next/app' import { ApolloProvider } from '@apollo/client' import { initializeApollo } from '../lib/apolloClient' function MyApp({ Component, pageProps }: AppProps) { const client = initializeApollo() return ( <ApolloProvider client={client}> <Component {...pageProps} /> </ApolloProvider> ) } export default MyAppこ |
こうすることでプロジェクト内のあらゆるコンポーネントでApollo Clientが使えるようになります。
コンポーネント側からの呼び出し:クエリ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { useQuery } from '@apollo/client' import { GET_USERS } from '../queries/queries' import { GetUsersQuery } from '../types/generated/graphql' // graphql-codegenで作られた型ファイル const FetchMain = () => { const { data, error } = useQuery<GetUsersQuery>(GET_USERS, { //fetchPolicy: 'network-only', fetchPolicy: 'cache-and-network', //fetchPolicy: 'cache-first', //fetchPolicy: 'no-cache', }) return ( <Main /> ) } |
fetchPolicy
基本最初に読み込みに行く画面につけるオプションになります。
値 | 説明 |
---|---|
cache-first | fetchPolicyの指定を省略した場合のデフォルト値、キャッシュが存在する場合はキャッシュを見にいく動きになります。
注意点としては、仮にサーバーサイドで新しいデータが生成されたとしても最初に取得したデータだけを表示し続けることになります。データが頻繁に変わるようなアプリケーションでは望ましくないです。 |
network-only | 毎回、キャッシュを使わずGraphQLのサーバーにアクセスするオプションです。さらに毎回cacheには結果を格納してくれます。
通信中は何もデータが表示されません。通信完了後に一気にデータが表示されます。 |
cache-and-network | network-onlyとよく似ています。一点違いがあるとすれば通信中はcacheのデータを表示してくれます。通信完了後は最新のデータを表示してくれるようになっています。多くのアプリケーションではこちらを選んでおけば問題ないです。 |
no-cache | これの場合、そもそもcacheにデータを格納しないので、別画面で@clientでcacheにアクセスしたとしても値を取得することができなくなります。毎回、サーバーにアクセスするので、通常のJSのaxiosでサーバーにアクセスする手法と似た手法になります。 |
キャッシュを読み込むかをクエリ側で制御する方法(@client)
すでにcacheがあることが前提の画面などで使う手法になります。
1 2 3 4 5 6 7 8 9 |
export const GET_USERS_LOCAL = gql` query GetUsers { users(order_by: { created_at: desc }) @client { id name created_at } } ` |
@clientというものをつけます。これをつけたクエリはキャッシュにあればそれを読み込んでくれます。
この場合は、コンポーネント側のfetchPolicyを指定したとしても意味をなさなくなります。
data
以下のように__typenameの部分もやってきます。
Apollo Clientではこの__typename、idをキーにしてcacheに保存されたデータを探しにくいメカニズムになっています。
コンポーネント側からの呼び出し:登録系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { useMutation } from '@apollo/client' import {CREATE_USER } from '../queries/queries' import { CreateUserMutation } from '../types/generated/graphql' const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) // 登録ボタン押下時の処理 const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() await insert_users_one({variables: {name: editedUser.name }}) } |
insertはApolloが自動でcacheを更新してくれないので更新処理を別途updateという関数を使って記述する必要があります。
insert_users_one
cache.identify
Apolloの機能です。新規で登録したデータのcacheのidを取得できます。
cache.modify({fields: {
Apolloのcacheを書き換えます。第一引数に書き換えたいフィールドを指定します。
toReference(cacheId)
Apolloの機能でIDに紐づいたデータをcacheから参照できます。既存のcacheの配列の先頭に今Hasuraに登録したデータを足しています。
コンポーネント側からの呼び出し:更新系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { useMutation } from '@apollo/client' import {UPDATE_USER } from '../queries/queries' import { UpdateUserMutation } from '../types/generated/graphql' const [update_users_by_pk] = useMutation<UpdateUserMutation>(UPDATE_USER) const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() try { await update_users_by_pk({ variables: { id: editedUser.id, name: editedUser.name, }, }) } } |
updateはApolloが自動でcacheを更新してくれます。
コンポーネント側からの呼び出し:削除系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { useMutation } from '@apollo/client' import {DELETE_USER } from '../queries/queries' import { DeleteUserMutation } from '../types/generated/graphql' const [delete_users_by_pk] = useMutation<DeleteUserMutation>(DELETE_USER, { update(cache, { data: { delete_users_by_pk } }) { cache.modify({ fields: { users(existingUsers, { readField }) { return existingUsers.filter( (user) => delete_users_by_pk.id !== readField('id', user) ) }, }, }) }, }) // 削除ボタン押下時の処理 onClick={async () => { await delete_users_by_pk({variables: {id: user.id }}) }} |
deleteはApolloが自動でcacheを更新してくれないので更新処理を別途updateという関数を使って記述する必要があります。
readField
Apolloの機能で、cacheから任意のフィールドの値を読むことができます。
この記事へのコメントはありません。