概要
Next.jsでApollo Client (@apollo/client
) を利用してSSR(SSG)する方法を調べていたのですが、色々な情報は見つかるものの、どれもうまく動かなくて苦労しました。
やりたかったこととしては下記となります。
- Server側でのレンダリング時もClient側と同じ useQuery を使いたい
- 調査時点での最新バージョン Next.js 10 + Apollo Client 3 で実現したい
- (初心者なので) あまり複雑なことはしたくない
結論としては next.js の公式サンプルを参考にするのが一番良さそうでした。
https://github.com/vercel/next.js/tree/canary/examples/with-apollo/
こちらのサンプル、ぱっと見だと何をやっているのか分かりにくかったので、調べた時のメモを記載しておきます。
ApolloProviderの設定
ここは Client 側で普通に Apollo Client を使う方法と基本的には同じです。Apollo Client の client は useApollo で作成しており、実装としては lib/apolloClient.js 内にあります。
export default function App({ Component, pageProps }) {
const apolloClient = useApollo(pageProps)
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
)
}
getStaticProps の実行
SSRしたいページで getStaticProps1 をしています。
ここはサーバ側でのみ(+生成時にのみ)実行されます。
export async function getStaticProps() {
const apolloClient = initializeApollo()
await apolloClient.query({
query: ALL_POSTS_QUERY,
variables: allPostsQueryVars,
})
return addApolloState(apolloClient, {
props: {},
revalidate: 1,
})
}
initializeApollo は useApollo と同じく lib/apolloClient.js で定義されており、Apollo Clientのclientを取得することができます。ポイントとなるのは、返ってきたclientを使って、**コンポーネントのレンダリング時に必要となるのと同じクエリー(ここでは ALL_POSTS_QUERY )**を実行する点です。
最後に addApolloState を実行して結果を PageProps として返しています。
addApolloState 定義も lib/apolloClient.js にあります。
apolloClient.js
次に各所で利用されていた apolloClient.js を見ていきます。
ここは若干複雑なのですが、コンセプトだけ再現できれば同じ実装にする必要はない気がしています。
addApolloState
getStaticProps が呼び出していた関数です。
現在の Apollo Client のキャッシュ内容を props の適当なキーに保存しています。この値は後ほど _app.js で useApollo を呼び出す際に pageProps として渡されます。(Server側とClient側の両方で使われる)
if (pageProps?.props) {
pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract()
}
useApollo
_app.js が呼び出していたHooksです。パラメータとして getStaticProps の結果(PageProps)を受け取っています。
pageProps から addApolloState で保存したキャッシュ内容を取り出した後、initializeApollo に渡しています。
initializeApollo から返された Apollo Client については、単純に _app.js 側に返すだけです。
(useMemo している理由は正直良く分かっていません。apolloClientはletで関数外に定義されているので結局あまり変わらない気もしているのですが・・・)
export function useApollo(pageProps) {
const state = pageProps[APOLLO_STATE_PROP_NAME]
const store = useMemo(() => initializeApollo(state), [state])
return store
}
initializeApollo
ここで重要なのは下記の部分で、addApolloState で保存した内容を現在の Apollo Client のキャッシュに復元しています2。これにより、以降の useQuery で getStaticProps で実行したのと同じクエリに対して getStaticProps での結果が返ってくるようになります3。
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract()
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
const data = merge(initialState, existingCache)
// Restore the cache with the merged data
_apolloClient.cache.restore(data)
}
Server Side の場合は毎回新規 Client を作成するという注意書きがあります。client を使い回すと、異なるリクエストで同じ client (cache) が再利用されてしまい、Paginationの結果がおかしくなるなどの副作用を避けるのが目的のようです。
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
まとめ
各処理を個別に見ていくと分かったような分からないような感じになるのですが、まとめると下記の流れのようになっているようです。Apollo Clientのキャッシュの保存と復元をうまく利用してServer Sideでの生成を実現するようですね。
Server Side
- getStaticPropsを実行
- 新規Apollo Clientを作成
- GraphQLクエリを実行 <== (await しているのでクエリ実行完了を待つ)
- Apollo ClientのキャッシュをPagePropsに保存
- Appからのツリーをレンダリング
- useApollo経由で新規Apollo Clientを作成 <== (getStaticPropsで保存した内容をキャッシュに復元)
- useQueryでGraphQLクエリを実行 <== (キャッシュに値があるので結果がすぐに返る)
- クエリ結果を使ってレンダリング
Client Side
- Appからのツリーをレンダリング
- useApollo経由で新規Apollo Clientを作成 <== (ServerSideから渡されたPagePropsに保存された内容をキャッシュに復元)
- useQueryでGraphQLクエリを実行 <== (キャッシュに値があるので結果がすぐに返る)
- クエリ結果を使ってレンダリング <== (クエリ結果がServer側と同じなので同じ結果になる(はず))
-
戻り値に revalidate の指定があるので厳密には SSR(SSG) ではなく Incremental Static Generation をしているはず・・・? ↩
-
キャッシュ内容を保存しておいて後から復元することを rehydration のような用語で表したりするようです ↩
-
厳密にはfetchPolicyとかが関係するので https://www.apollographql.com/docs/react/performance/server-side-rendering/#store-rehydration 等にその辺りのことが書いてある気がします ↩