🪴

(記事一覧編)jQuery使いのエンジニアがReactとNext.jsでWebサイト制作するために最低限押さえておきたいコード

2023/01/27に公開

こちらの記事は以下の記事の続きです。
jQuery使いのエンジニアがReactとNext.jsでWebサイト制作するために最低限押さえておきたいコード(基礎編)

今回は前回ほど長くありませんが、その代わりにちょっとレベルが上がっています。

前回のおさらい

前回は

  • コンポーネントの作成
  • propsの渡し方、受け取り方、分割代入
  • mapを使って配列からコンポーネントを表示

このあたりをやりました。

今回やること

  • async await
  • getStaticProps
  • 記事一覧を作る

SSG

Webサイト制作ではSEOを考慮する必要があり、SSG(静的サイト生成)がほぼ必須です。
ヘッドレスCMSなどからAPIを叩いて(関西人はしばくかもしれません)データを取得し、取得したデータをもとに記事一覧や記事詳細などのページを生成する場合がほとんどでしょう。

それに使用するのがgetStaticPropsgetStaticPathsという特別な名前の関数で、こいつをページコンポーネント(src/pages配下のファイル)からexportする必要があります。

今回も記事がちょっとだけ長いので、この記事内ではgetStaticPropsだけ説明してgetStaticPathsについてはまた次の記事で説明します。

記事一覧(/news/)

まず記事一覧ページを作りたいので、src/pages/news/index.tsxを作成しましょう。

このtsxファイル内では、記事の一覧を取得してそのデータをもとに記事コンポーネントを表示したいので、先ほど軽く説明したgetStaticPropsを使用します。

データの取得はgetStaticProps内で行う必要があり、この関数はページコンポーネントからしか呼べません。

基本的な形は以下です。

export const getStaticProps = async () => {
  const { posts } = await 記事データを取得する非同期関数()
  return {
    props: { posts }
  }
}

記事データを取得する非同期関数()の部分ですが、同じようなデータを取得する記述を何度も書いてしまうと修正が面倒ですし、コピペコピペになってしまうとよくないので、適当な関数に閉じ込めて別ファイルに切り分けます。

その前に新しく出てきたasync awaitが気になると思うので軽く説明します。
ざっくり説明しているのでちゃんと学びたい人はほかの記事で学んでください。

async

asyncがついた関数はPromiseを返します。

ちょっとよくわからないかもしれませんが、簡単に言うと

  • データを取得するのに時間がかかります!その間は「データの取得まだ終わってないで~」
  • データの取得に失敗したら「失敗したわ…」
  • 成功したら「データとってきたで~これを受け取れ!」

というのを教えてくれるオブジェクトであるPromiseを返す関数です。

このasyncがついた(Promiseを返す)関数はawaitを使うと値を簡単にいい感じに取り出すことができます。
いい感じに の部分が気になる人はPromise async awaitとかでググってみてください。

例はこんな感じです。

const fetchPosts = async () => {
  const res = await fetch('データ取得先のURL')
  const posts = await res.json()
  return { posts }
}

fetchの部分はデータを取得するまで時間がかかるPromiseを返す関数です。

データの取得まだ終わってないで~の段階でそれを教えられてもどうしようもないので、取得に失敗するか、それとも成功してデータが返ってくるのか まで待つ必要があり、それをやってくれるのがawaitです。

ノブのいう「ちょっと待て」です。

fetchPostsの処理を追っていってみましょう。

await

awaitは先ほど説明した通りデータの取得が終わるまで「ちょっと待て」をやってくれるもので、asyncがついた関数の内部で使用することができます。

先ほどの例を見ると

const fetchPosts = async () => { //asyncがついた関数
  const res = await fetch('データ取得先のURL') // awaitが使える
  const posts = await res.json() // resはawaitされているので、データの取得が終わったらここが実行される
  return { posts }
}

こうなっているので、

  1. fetchするからawaitしといてくれ
  2. posts「了解。終わったらresって箱に入れといて」
  3. posts「resが来たわ jsonにするで~」
  4. return「json()し終わったらpostsって名前で返しとくわ」

こんな感じでヘッドレスCMSなどが提供しているAPIから非同期でデータを取得します。
データの取得はgetStaticPropsの中でやると書きましたが、こいつにもasyncがついていましたね。

記事データを取得する非同期関数

これをまだ作っていなかったので作りましょう。
ダミーのデータを返してくれるAPIがあるのでこちらを利用します。

https://jsonplaceholder.typicode.com/posts

src/utils/fetchPosts.ts
type Post = { id: number, title: string }
type Posts = { posts: Post[] }
const fetchPosts = async (): Promise<Posts> => {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts')
  const posts = await res.json()
  return { posts }
}

こうです。
: Promise<Posts>の部分はTypeScriptの型の情報で、postsっていうプロパティを持ったオブジェクトがPromiseでデータ取得できたら{ id: number, title: string }こういうオブジェクトが集まった配列が返ってくるで みたいな型です。
ちょっと日本語がわかりにくい気もしますが、要するに記事のデータはこういう配列で返ってくるで~みたいなもんです。

どんなデータが返ってくるのか型をつけておかないとコンポーネントに渡すデータがあっているかわからなくなってしまうので基本的に型はつけます。
TypeScriptをかけないときはこれがあると逆にわかりにくく感じる場合もあると思いますが、逆にこれがないとどこで詰まっているのかわからなくなってくるので頑張って書いたほうがいいです。

これで記事取得関数ができたので、記事一覧ページを表示できそうです。
いきなり難しくなりますが以下のようになります。
型の部分がちょっとややこしいですが、それ以外は今までに学習したことしか使っていません。

src/pages/news/index.tsx
import type { NextPage, GetStaticProps, InferGetStaticPropsType } from 'next'
import NextLink from 'next/link'
import { fetchPosts } from '../../utils/fetchPosts'
type Props = Awaited<ReturnType<typeof fetchPosts>>
export const getStaticProps: GetStaticProps<Props> = async () => {
	const { posts } = await fetchPosts()
	return {
		props: { posts },
	}
}
const News: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = ({ posts }) => {
	return (
		<ul>
			{posts.map((post) => {
				return <li key={post.id}><NextLink href={`/posts/${id}`}>{ title }</NextLink></li>
			})}
		</ul>
	)
}
export default News

ちょっとずつ見ていきましょう。

まず

import type { NextPage, GetStaticProps, InferGetStaticPropsType } from 'next'
import NextLink from 'next/link'
import { fetchPosts } from '../../utils/fetchPosts'

この部分は1行目が型のimport、2行目はリンクコンポーネントです。
3行目がさっき作った関数のimportですね。

1行目はfrom 'next'しておけばサジェストされるので名前だけふんわり覚えておけばOKです。

type Props = Awaited<ReturnType<typeof fetchPosts>>

この部分はTypeScriptの記述で、typeof fetchPostsの部分でfetchPosts関数の型を取得します。

次にReturnTypeを使ってこの関数がどんな情報(型)を返すんですか?というのを取得します。
この段階でReturnType<typeof fetchPosts>ここの部分はfetchPosts関数の返り値のPromise<Posts>型になっています。

ここからPostsの部分の型が欲しいので、Awaited型を使用します。
最終的に

type Props = {
  posts: { id: number, title: string }[]
}

が得られます。

fetchPosts関数からtype Postsexportしておけばまぁそれでもいい気もしますが、import部分が長くなったりであんまりいい気がしないので、今回はちょっと難しそうに見える型を使ってみました。
1個ずつ見ていくと特に難しくはないので落ち着いてみましょう。

export const getStaticProps: GetStaticProps<Props> = async () => {
	const { posts } = await fetchPosts()
	return {
		props: { posts },
	}
}

ここの部分はすでに説明が終わっているので型のGetStaticProps<Props>の部分を一応説明すると、propsがどんなデータ(型)を返しますか?というのがGetStaticProps型で、<>の部分にさっき作ったProps型を指定することによってpropsがどんなデータを返すのかを教えています。

これでpropsにpostsのデータが入るので後は表示するだけです。

const News: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = ({ posts } // propsから分割代入しています) => {
	return (
		<ul>
			{ posts.map(( { id, title } ) => {
				return <li key={ id }><NextLink href={`/posts/${id}`}>{ title }</NextLink></li>
			}) }
		</ul>
	)
}
export default News

この部分です。
: NextPage<InferGetStaticPropsType<typeof getStaticProps>>の部分は型の説明です。覚えるでOKかと思います。getStaticPropsに型がちゃんとついていればそのほかは暗記で問題ありません。

ここで型を書いてあげることによって、({ posts })の部分に型がつくのでpostsっていうプロパティが入ってきているんだな ということがわかり、postsにも型がついているのでmapで回してコンポーネントにデータを渡すときも型の恩恵を受けることができます。

posts.mapの部分は前回の記事で学習した、配列からJSXの新しい配列を返して表示する部分です。
keyを忘れないようにしましょう。(もし忘れていてもエディタが怒ってくれます)

できた~

これで記事一覧ページを作ることができました。

実際は記事を全件ではなく10件くらい取得してページネーションをつけたりサイドバーにカテゴリ一覧を表示したりすることになると思います。

ページネーションは記事の件数と一ページの表示件数、現在何ページ目か、リンクを何個表示するか の情報があれば計算して表示できるのでやってみてください。

サイドバーにカテゴリ一覧を表示するのも、記事一覧を取得したのと同じようにデータを取得してgetStaticPropspropsの部分で

return {
  props: {
    posts, categories
  }
}

みたいに複数返してあげて、

const News: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = ({ posts, categories }) =>

みたいにすればOKです。

まとめ

今回はgetStaticPropsの使い方と、async awaitを学びました。
この辺ができればデータの取得はできるので簡単な一覧ページの表示くらいはできるようになっているはずです。

記事詳細のデータも同じような感じです。
TypeScript周りがよくわからなかった人はサバイバルTypeScriptを見るのをおすすめします。

お疲れさまでした!

次回

この記事内で記事詳細ページやPromise.allを使ったカテゴリごとの記事一覧取得くらいまでやろうと思っていたのですが、思ったより長くなってしまったので以下は次の記事で説明します。

  • getStaticPaths
  • Promise.all
  • [slug].tsx

Discussion