📚

Next.js+Apollo Client+Contentfulで「もっと読み込む」ボタンをつける

7 min read

Contentful+Next.jsで記事一覧を作る際に、Apollo ClientのfetchMoreを使って 「もっと読み込む」 機能をつけます。

postという種類の投稿がある前提です。

動作例

3件まで読んでいる場合。

ロード後。

筆者はGraphQL/Apolloを触ったばかりなので、型生成などで非効率なやり方をしている可能性があります。

GraphQLの準備

https://zenn.dev/sasigume/articles/5afb7e1c6a00ad

この記事を参考にしてください。

以下、

  • src/graphql/post.tsALLPOST_QUERYというクエリを定義してある
  • src/graphql/generated/AllPostQueryAllPostQuery, AllPostQueryVariablesを生成している

前提で進めます。

Apollo Clientの準備

クライアント部分の実装

https://github.com/vercel/next.js/tree/canary/examples/with-apollo

https://www.rockyourcode.com/nextjs-with-apollo-ssr-cookies-and-typescript/

上記の記事を参考に、src/lib/apollo.tsにApollo関連の関数を書いてください。

  • createApolloClient: 初期化と設定
  • initializeApollo: Next.jsにうまいこと適応させる
  • addApolloState: getStaticProps/getServerSidePropsで活用する
  • useApollo: PagePropsを活用する

が必要です。

src/lib/apollo.ts
const createApolloClient = (preview = false) => {
  return new ApolloClient({
    // SSRはサーバーのみ
    ssrMode: typeof window === 'undefined',
    link: new HttpLink({
      uri: `https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`,
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${preview ? process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN : process.env.CONTENTFUL_ACCESS_TOKEN}`,
      },
    }),
    cache: new InMemoryCache({
      typePolicies: {
        Query: {
          fields: {
            // https://stackoverflow.com/a/66581601
            postCollection: {
              keyArgs: false,
              // 記事を追加で読み込み
              merge(existing, incoming) {
                if (!incoming) return existing;
                if (!existing) return incoming;
                const { items, ...rest } = incoming;
                let result = rest;
                result.items = [...existing.items, ...items];
                return result;
              },
            },
          },
        },
      },
    }),
  });
};

その際、このようにキャッシュのルールを設定しなければなりません。

postCollectionitemsをうまいことmergeすることで、正しく結果がマージされます。

https://www.apollographql.com/docs/react/caching/cache-field-behavior/

typePoliciesの書き方に関しては、他にも色々できるようです。

linkedFromがうまくいかない

この部分ですが、ContentfulのlinkedFromと併用しようとしてもうまくいきませんでした。

誰か助けて
src/graphql/tag.ts
export const TAG_QUERY = gql`
  query TagQuery($id: String!, $preview: Boolean, $postSkip: Int, $postLimit: Int) {
    tag(id: $id, preview: $preview) {
      sys {
        id
      }
      title
      linkedFrom {
        postCollection(skip: $postSkip, limit: $postLimit) {
          items {
            title
            sys {
              id
              firstPublishedAt
              publishedAt
            }
          }
          total
        }
      }
    }
  }
`;

こういう状態なんですけど、これのpostCollection.itemsのマージがうまくいきません...

この部分の参考

https://stackoverflow.com/a/66581601

環境変数をクライアントに露出

next.config.js
  env: {
+    CONTENTFUL_SPACE_ID: process.env.CONTENTFUL_SPACE_ID,
+    CONTENTFUL_ACCESS_TOKEN: process.env.CONTENTFUL_ACCESS_TOKEN,
+    CONTENTFUL_PREVIEW_ACCESS_TOKEN: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN,
  },

これ忘れてもページ生成はできるはずです。ただクライアント側でフェッチができません。

_app.tsxの編集

pagePropsを活用します。

src/pages/_app.tsx
import 'src/styles/globals.css';
import { ApolloProvider } from '@apollo/client';
import { useApollo } from 'src/lib/apollo';
import type { AppProps } from 'next/app';

function MyExtremeSuperStrongBeautifulApp({ Component, pageProps }: AppProps) {
  const apolloClient = useApollo(pageProps);
  return (
    <ApolloProvider client={apolloClient}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}
export default MyExtremeSuperStrongBeautifulApp;

記事一覧コンポーネントを作成

https://github.com/vercel/next.js/blob/canary/examples/with-apollo/components/PostList.js

こちらをベースにしています。

src/components/List.tsx
import { AllPostQuery, AllPostQueryVariables } from 'src/graphql/generated/AllPostQuery';
import { ALLPOST_QUERY } from 'src/graphql/post';

import { useQuery, NetworkStatus } from '@apollo/client';

export const postListQueryVars = { skip: 0, limit: 3 };

const List: React.FC = () => {
  const { data, loading, error, networkStatus, fetchMore } = useQuery<AllPostQuery, AllPostQueryVariables>(ALLPOST_QUERY, {
    variables: postListQueryVars,
    notifyOnNetworkStatusChange: true,
  });

  // まだ記事があるか?
  const loadingMorePosts = networkStatus === NetworkStatus.fetchMore;
  const length = data?.postCollection?.items?.length ?? 0;
  const total = data?.postCollection?.total ?? 0;
  const areMorePosts = length < total;
  const loadMorePosts = () => {
    fetchMore({
      variables: {
        skip: length,
      },
    });
  };

  if (error) return <div>ERROR: {JSON.stringify(error)}</div>;
  if (loading && !loadingMorePosts) return <div>LOADING...</div>;
  return (
    <div className="flex flex-col gap-3">
      {data?.postCollection?.items.map((p, n) => (
        <div key={p ? p.sys.id : n}>{p.title}</div>
      ))}

      {areMorePosts && (
        <button disabled={loadingMorePosts} onClick={loadMorePosts}>
          {loadingMorePosts ? 'Loading...' : 'Show More'}
        </button>
      )}
    </div>
  );
};

export default List;

読み込み中はnetworkStatus === NetworkStatus.fetchMoreが真になります。fetchMoreを押すと、クエリのskip変数が変わるため、さらに記事を読み込むことができます。

記事一覧ページを作成

src/pages/index.tsx
import List, { postListQueryVars } from 'src/components/List';
import { addApolloState, initializeApollo } from 'src/lib/apollo';
import { NextPage } from 'next';
import { ALLPOST_QUERY } from 'src/graphql/post';
import { AllPostQuery, AllPostQueryVariables } from 'src/graphql/generated/AllPostQuery';
const client = initializeApollo();

export async function getStaticProps() {

  // 記事一覧と同じクエリを使う
  await client.query<AllPostQuery, AllPostQueryVariables>({
    query: ALLPOST_QUERY,
    variables: postListQueryVars,
  });

  return addApolloState(client, {
    props: {},
    revalidate: 60,
  });
}

const Home: NextPage = () => {
  return (
    <>
      <List />
    </>
  );
};

export default Home;

普通はgetStaticPropsで記事を取得し、propsに渡しますよね。Apollo Clientを活用するためにこうなっています。

忘れちゃいけないのが、getStaticProps内で一覧コンポーネントと同じクエリを使うことです。こうすることでサーバーサイドでもキャッシュが活用されます。

https://github.com/sophiabrandt/nextjs-ecommerce/blob/main/frontend/pages/product/[id]/index.tsx

なお個別記事ページでは、記事のslugやIDといったデータをaddApolloStateで返して利用しましょう。

ページ実装の参考

https://qiita.com/madogiwa/items/27c5b71ee6038803e5e3

https://github.com/vercel/next.js/blob/canary/examples/with-apollo/pages/index.js
GitHubで編集を提案

Discussion

ログインするとコメントできます