りゅーそう
元高校地歴科教員。現在microCMSでエンジニアをしています。
Saitama.jsというLT会を運営中です。
発言はすべて個人の意見です。
jamstack
blog
2020/09/05
2022/01/23
タイトルの通り、Jamstackのブログをリファクタリングしました。
ブログを開発していくうちにコードがなかなかめちゃくちゃなことになってきたので、コンポーネントに分けて再利用しやすくしたりしました。
エンジニアを本職にしている方々に取っては呼吸をするかのように行っていることだとは思いますが、この記事はわざわざそれを記事にしてみます。
なお、私は本職はエンジニアではないので参考までに。(もしもっと良い方法があったら教えてください)
いわゆるJamstack。Gatsby(TypeScript)+ microCMSで開発しています。現在ご覧になっているブログの開発です。
まずはブログでは良くありがちな構成だとは思いますが、以下のようなページを作成することが良くあると思います。
記事一覧をブログではトップページに持ってきますね。
こちらの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で生成して各記事を参照し、グループにして表示することも行っています。これもブログでは良くあるやつですね。
コードは以下のようになります。
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を作成するイメージ。レイアウトは各ページで行います。
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を参照するのはやめました。理由は以下の通りです。
これらのリファクタリングをして得たメリットを最後に共有します。
もちろん、コードの再利用が出来、短くなったことによってコードが見やすくなったと言うことはもちろんですが、以下のようなメリットがありました。
コンポーネントに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内のコードの修正のみで解決できます。
リファクタリング大事。
もっと良いやり方ありましたらご教授いただけるとありがたいです。
今後もブログ開発頑張ります。