タイトルの通り、Jamstackのブログをリファクタリングしました。
ブログを開発していくうちにコードがなかなかめちゃくちゃなことになってきたので、コンポーネントに分けて再利用しやすくしたりしました。
エンジニアを本職にしている方々に取っては呼吸をするかのように行っていることだとは思いますが、この記事はわざわざそれを記事にしてみます。
なお、私は本職はエンジニアではないので参考までに。(もしもっと良い方法があったら教えてください)

環境

いわゆるJamstack。Gatsby(TypeScript)+ microCMSで開発しています。現在ご覧になっているブログの開発です。

やること

まずはブログでは良くありがちな構成だとは思いますが、以下のようなページを作成することが良くあると思います。
記事一覧をブログではトップページに持ってきますね。

Postページ

こちらのUIを表したコードはこちらになります。
Gatsbyはgraphqlを用いて、microCMSからデータを取得します。

src/templates/posts.tsx
const Posts: React.FC<Props> = ({
  data,
  location,
  pageContext,
}) => {
  return (
    <>
      <SEO
        pagetitle="POST"
        pagedesc="技術ブログのページ"
        pagepath={location.pathname}
      />
      <Title>POST</Title>
      <section css={PostList}>
        {data.allMicrocmsPosts?.edges?.map((edge) => {
          const posts = edge.node;
          return (
            <React.Fragment key={posts.id}>
              <div css={PostItem}>
                <Link to={`/posts/${posts.postsId}`}>
                  <article>
                    <p className="PostItemTitle">
                      {posts.title}
                    </p>
                    {posts?.fields?.featuredImage
                      ?.fluid && (
                      <Image
                        fluid={
                          posts.fields.featuredImage.fluid
                        }
                        alt="ブログのイメージ画像"
                      />
                    )}
                    <div className="PostItemTag">
                      {posts?.tags?.map(
                        (tag) =>
                          tag?.id && (
                            <React.Fragment key={tag.id}>
                              <Link to={`/tags/${tag.id}`}>
                                <span>{tag.name}</span>
                              </Link>
                            </React.Fragment>
                          ),
                      )}
                    </div>
                    <div className="PostItemDay">
                      <div className="PostItemDayItem">
                        <FaCalendar className="icon" />
                        投稿:
                        {posts.createdAt}
                      </div>
                      <div className="PostItemDayItem">
                        <FaRegCalendarCheck className="icon" />
                        更新:
                        {posts.updatedAt}
                      </div>
                    </div>
                  </article>
                </Link>
              </div>
            </React.Fragment>
          );
        })}
      </section>
      <div css={PostPageNation}>
        {!pageContext.isFirst && (
          <div className="PostPageNationPrev">
            <Link
              to={
                pageContext.currentPage === 2
                  ? `/posts/`
                  : `/posts/${pageContext.currentPage - 1}`
              }
              rel="prev"
            >
              <FaArrowCircleLeft className="icons" />
              <span>前のページ</span>
            </Link>
          </div>
        )}
        {!pageContext.isLast && (
          <div className="PostPageNationNext">
            <Link
              to={`/posts/${pageContext.currentPage + 1}/`}
              rel="next"
            >
              <span>次のページ</span>
              <FaArrowCircleRight className="icons" />
            </Link>
          </div>
        )}
      </div>
    </>
  );
};


クエリは以下のようになります。


export const pageQuery = graphql`
  query PagePosts($skip: Int!, $limit: Int!) {
    allMicrocmsPosts(
      sort: { fields: createdAt, order: DESC }
      skip: $skip
      limit: $limit
    ) {
      edges {
        node {
          id
          postsId
          title
          createdAt(locale: "ja", formatString: "YYYY/M/DD")
          updatedAt(locale: "ja", formatString: "YYYY/M/DD")
          tags {
            id
            name
          }
          fields {
            featuredImage {
              fluid(maxHeight: 120, maxWidth: 360) {
                src
                sizes
                base64
                aspectRatio
                srcSet
                srcSetWebp
                srcWebp
              }
            }
          }
          content
        }
      }
    }
  }
`;


また当ブログでは「React」や「ブログ開発」などのtagをmicroCMSで生成して各記事を参照し、グループにして表示することも行っています。これもブログでは良くあるやつですね。

Reactのタグがついたページ

コードは以下のようになります。

src/templates/tags.tsx
const Tags: React.FC<Props> = ({
  data,
  location,
  pageContext,
}) => (
  <>
    <SEO
      pagetitle={pageContext.tagsname}
      pagedesc={`カテゴリー別ページ | ${pageContext.tagsname}`}
      pagepath={location.pathname}
    />
    <Title>{pageContext.tagsname}</Title>
    <section css={PostList}>
      {data.allMicrocmsPosts?.edges?.map((edge) => {
        const posts = edge.node;
        return (
          <React.Fragment key={posts.id}>
            <div css={PostItem}>
              <Link to={`/posts/${posts.postsId}`}>
                <article>
                  <p className="PostItemTitle">
                    {posts.title}
                  </p>
                  {posts?.fields?.featuredImage?.fluid && (
                    <Image
                      fluid={
                        posts.fields.featuredImage.fluid
                      }
                      alt="ブログのイメージ画像"
                    />
                  )}
                  <div className="PostItemTag">
                    {posts?.tags?.map(
                      (tag) =>
                        tag?.id && (
                          <React.Fragment key={tag.id}>
                            <Link to={`/tags/${tag.id}`}>
                              <span>{tag.name}</span>
                            </Link>
                          </React.Fragment>
                        ),
                    )}
                  </div>
                  <div className="PostItemDay">
                    <div className="PostItemDayItem">
                      <FaCalendar className="icon" />
                      <p>
                        投稿:
                        {posts.createdAt}
                      </p>
                    </div>
                    <div className="PostItemDayItem">
                      <FaRegCalendarCheck className="icon" />
                      <p>
                        更新:
                        {posts.updatedAt}
                      </p>
                    </div>
                  </div>
                </article>
              </Link>
            </div>
          </React.Fragment>
        );
      })}
    </section>
    <div>
      {!pageContext.isFirst && (
        <div>
          <Link
            to={
              pageContext.currentPage === 2
                ? `/posts/`
                : `/posts/${pageContext.currentPage - 1}`
            }
            rel="prev"
          >
            <span>前のページ</span>
          </Link>
        </div>
      )}
      {!pageContext.isLast && (
        <div>
          <Link
            to={`/posts/${pageContext.currentPage + 1}/`}
            rel="next"
          >
            <span>次のページ</span>
          </Link>
        </div>
      )}
    </div>
  </>
);


お気づきになられたと思いますが、postsページとtagsページのコードがかなり重複してしまっています。このコードを共通化します。
ちなみにGatsbyでブログを作成する記事は以前書かせていただいたので、ご参照ください。
Gatsbyで型安全なブログ開発
また以下の本が詳細に書かれているので、おすすめです。
Webサイト高速化のための静的ジェネレーター活用入門

今回のサンプルコードはこちらです。(と言うか当ブログのコードです。スターください)
https://github.com/YouheiNozaki/PortfolioSite

実装

では実際にやっていきましょう。
Jamstack構成でheadlessCMSからデータをフェッチするのは良くあるので、ぜひ参考にしてみてください。
Atomic design的な構成で当ブログはディレクトリ構成を分けているので、
src/molecules/Card/index.tsxに以下のようなコンポーネントのコードを書いていきます。
先ほどのPostページのCardを作成するコンポーネントになります。
※以下のPOST一覧のCardを作成するイメージ。レイアウトは各ページで行います。

POSTページ

import * as React from 'react';
import { Link } from 'gatsby';
import Image from 'gatsby-image';
import {
  FaCalendar,
  FaRegCalendarCheck,
} from 'react-icons/fa';

type Props = {
  postsId: string | null | undefined;
  title: string | null | undefined;
  fluidImage: any;
  createdAt: Date;
  updatedAt: Date;
};


export const Card: React.FC<Props> = ({
  postsId,
  title,
  fluidImage,
  createdAt,
  updatedAt,
}) => {

  return (
      <div>
        <Link to={`/posts/${postsId}`}>
          <article>
            <p className="PostItemTitle">{title}</p>
            <Image
              fluid={fluidImage}
              alt="ブログのイメージ画像"
            />
            <div className="PostItemDay">
              <div className="PostItemDayItem">
                <FaCalendar className="icon" />
                投稿:
                {createdAt}
              </div>
              <div className="PostItemDayItem">
                <FaRegCalendarCheck className="icon" />
                更新:
                {updatedAt}
              </div>
            </div>
          </article>
        </Link>
      </div>
  );
};


ポイントはTypeScriptを導入してコンポーネントに与えるPropsに型を与えていることです。
postsIdとtitleには string | null | undefined と言うnullとundefinedも許容する型を与えています。これはGatsbyのgatsby-plugin-graphql-codegenの生成する型がこのようになっているためです。
もっと厳密にやりたい方はstringのみにすると良いでしょう。(その場合はCardコンポーネントを使用する親コンポーネントでこれらの値を使用する際typeguardやoptional chainningを使用してnullとundefinedを弾きます。)
fluidImageはgatsby-imageを使用して表示させる画像のPropsです。これは敗北です。gatsby-imageの良い型の付け方ありましたら教えてください。

type Props = {
  postsId: string | null | undefined;
  title: string | null | undefined;
  fluidImage: any;
  createdAt: Date;
  updatedAt: Date;
};


これらのPropsを使用して、UIを作成するコンポーネントを作成しています。

export const Card: React.FC<Props> = ({
  postsId,
  title,
  fluidImage,
  createdAt,
  updatedAt,
}) => {

  return (
      <div>
        <Link to={`/posts/${postsId}`}>
          <article>
            <p className="PostItemTitle">{title}</p>
            <Image
              fluid={fluidImage}
              alt="ブログのイメージ画像"
            />
            <div className="PostItemDay">
              <div className="PostItemDayItem">
                <FaCalendar className="icon" />
                投稿:
                {createdAt}
              </div>
              <div className="PostItemDayItem">
                <FaRegCalendarCheck className="icon" />
                更新:
                {updatedAt}
              </div>
            </div>
          </article>
        </Link>
      </div>
  );
};


このCardコンポーネントは以下のように使用します。

//
const Posts: React.FC<Props> = ({
  data,
  location,
  pageContext,
}) => {
  return (
    <>
      <SEO
        pagetitle="POST"
        pagedesc="技術ブログのページ"
        pagepath={location.pathname}
      />
      <Title color={colors.lightBlue}>POST</Title>
      <section>
        {data.allMicrocmsPosts?.edges?.map((edge) => {
          const posts = edge.node;
          return (
            <React.Fragment key={posts.id}>
              <Card
                postsId={posts.postsId}
                title={posts.title}
                fluidImage={
                  posts.fields?.featuredImage?.fluid
                }
                createdAt={posts.createdAt}
                updatedAt={posts.updatedAt}
              />
            </React.Fragment>
          );
        })}
      </section>
    </>
  );
};


const Tags: React.FC<Props> = ({
  data,
  location,
  pageContext,
}) => (
  <>
    <SEO
      pagetitle={pageContext.tagsname}
      pagedesc={`カテゴリー別ページ | ${pageContext.tagsname}`}
      pagepath={location.pathname}
    />
    <Title color={colors.lightBlue}>
      {pageContext.tagsname}
    </Title>
    <section css={PostList}>
      {data.allMicrocmsPosts?.edges?.map((edge) => {
        const posts = edge.node;
        return (
          <React.Fragment key={posts.id}>
            <Card
              postsId={posts.postsId}
              title={posts.title}
              fluidImage={
                posts.fields?.featuredImage?.fluid
              }
              createdAt={posts.createdAt}
              updatedAt={posts.updatedAt}
            />
          </React.Fragment>
        );
      })}
    </section>
  </>
);


コンポーネントを使用することで、コードが再利用でき、pageを構成するコンポーネントがシンプルになってみやすくなりました!
ちなみにこれらのカードをレイアウトするにはpagesにグリッドレイアウトを指定します。以下のようなコードです。(emotionを使用しています)

export const PostList = css({
  display: 'grid',
  gridTemplateColumns: '1fr 1fr',
  gap: sizes[4],
  [mq[0]]: {
    display: 'block',
  },
});


こちらは以下のように親コンポーネントに渡します。

<section css={PostList}>
        {data.allMicrocmsPosts?.edges?.map((edge) => {
          const posts = edge.node;
          return (
            <React.Fragment key={posts.id}>
              <Card
                postsId={posts.postsId}
                title={posts.title}
                fluidImage={
                  posts.fields?.featuredImage?.fluid
                }
                createdAt={posts.createdAt}
                updatedAt={posts.updatedAt}
              />
            </React.Fragment>
          );
        })}
 </section>


コンポーネントを作成するときはレイアウトやmarginなどは親コンポーネントで実装することを意識すると修正がしやすいコードになると思っています。

ちなみにPOSTのCard内でtagを参照するのはやめました。理由は以下の通りです。

  • tagsを表示させるためのAPIをmicroCMSから呼び出さなくてはならず、実装コストがかかる(具体的に言うとuseStaticQueryを用いてcomponentのなかでgraphqlを実装しなくてはいけない)
  • そもそもアクセシビリティ的によろしくない。Cardをクリックしたと思ったら、tagのページ先に遷移してしまったなどの予期せぬ結果をもたらす可能性があった。(実際にGoogle Chromeさんに怒られていた)


メリット

これらのリファクタリングをして得たメリットを最後に共有します。
もちろん、コードの再利用が出来、短くなったことによってコードが見やすくなったと言うことはもちろんですが、以下のようなメリットがありました。

アニメーションやライブラリの使用

コンポーネントにCardのUIを構成する責務を分けることによって、ライブラリの使用に踏み切れました。
具体的には以下のライブラリを使用しました。
https://ryusou.dev/posts/react-intersection-observer
こちらの記事で言及したライブラリはHooksを用いてコンポーネント内で挙動を制御するライブラリです。
コンポーネントに分けることで以下のようなコードを書くことができました。

import * as React from 'react';
import { Link } from 'gatsby';
import Image from 'gatsby-image';
import {
  FaCalendar,
  FaRegCalendarCheck,
} from 'react-icons/fa';
import { useInView } from 'react-intersection-observer';

import {
  sizes,
  colors,
  typography,
  mq,
} from '../../../theme';
import { BottomIn } from '../../../keyframes';

type Props = {
  postsId: string | null | undefined;
  title: string | null | undefined;
  fluidImage: any;
  createdAt: Date;
  updatedAt: Date;
};

export const Card: React.FC<Props> = ({
  postsId,
  title,
  fluidImage,
  createdAt,
  updatedAt,
}) => {
  const [ref, inView] = useInView({
    rootMargin: '-50px 0px',
  });

  return (
    <>
      <div
        css={{
          display: 'grid',
          gridTemplateColumns: '1fr 1fr',
          opacity: inView ? 1 : 0,
          animation: inView
            ? `${BottomIn} 0.5s ease-out`
            : 0,
          [mq[0]]: {
            padding: sizes[4],
          },
          '& a': {
            textDecoration: 'none',
            cursor: 'pointer',
            '& article': {
              border: `solid ${sizes[1]} ${colors.lightBlue}`,
              borderRadius: sizes[2],
              padding: sizes[4],
              width: sizes.largeSizes.sm,
              [mq[1]]: {
                width: sizes.largeSizes.xs,
              },
              [mq[0]]: {
                width: sizes.largeSizes.xs,
              },
              '& .PostItemTitle': {
                color: colors.blue,
                fontWeight: typography.fontWeights.medium,
                textOverflow: 'ellipsis',
                overflow: 'hidden',
                whiteSpace: 'nowrap',
              },
              '& img': {
                borderRadius: sizes[2],
              },
              '& .PostItemDay': {
                marginTop: sizes[3],
                display: 'flex',
                [mq[0]]: {
                  marginTop: sizes[1],
                },
                '& .PostItemDayItem': {
                  display: 'flex',
                  color: colors.blue,
                  marginLeft: sizes[2],
                  '& .icon': {
                    marginRight: sizes[1],
                  },
                },
                [mq[1]]: {
                  display: 'block',
                },
                [mq[0]]: {
                  display: 'block',
                },
              },
            },
          },
        }}
        ref={ref}
      >
        <Link to={`/posts/${postsId}`}>
          <article>
            <p className="PostItemTitle">{title}</p>
            <Image
              fluid={fluidImage}
              alt="ブログのイメージ画像"
            />
            <div className="PostItemDay">
              <div className="PostItemDayItem">
                <FaCalendar className="icon" />
                投稿:
                {createdAt}
              </div>
              <div className="PostItemDayItem">
                <FaRegCalendarCheck className="icon" />
                更新:
                {updatedAt}
              </div>
            </div>
          </article>
        </Link>
      </div>
    </>
  );
};


このようなコードは流石にpages要素の中では書けないです。インラインCSSを書いたりすることができるようになりました。また、今後ライブラリの使用をやめたい際にはcomponent内のコードの修正のみで解決できます。

まとめ

リファクタリング大事。
もっと良いやり方ありましたらご教授いただけるとありがたいです。
今後もブログ開発頑張ります。