MacでNext.jsのGettingStartedを進めてみる (3)

Macで [Next.jsの公式Getting Started](https://nextjs.org/docs/getting-started)を進めてみたメモ。

使用した環境は Intel Mac + Catalinaです。
バックエンドとしては[strapi](https://strapi.io/)を使っていきます。

[前回の記事で Basic Features / Pages の章をやりました。](https://qiita.com/tk_hamaguchi/items/2e375ce4a3037c7e9c77)
今回は Basic Features / Data Fetching やっていきます。

# 3. Basic Features / Data Fetching をやってみる

## \`getStaticProps\` (Static Generation)

Simple Example に TypeScript: Use GetStaticProps を組み合わせて、 `pages/blog.tsx` を変えてみる

```diff_tsx:pages/blog.tsx
+ import { GetStaticProps } from 'next'
+ import { InferGetStaticPropsType } from 'next'
+ 
+ type Post = {
+   author: string
+   content: string
+   title: string
+ }
+ 
- function Blog({ posts }: {posts: any}) {
+ function Blog({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
    return (
      <ul>
-       {posts.map((post: any) => (
+       {posts.map((post: Post) => (
          <li>{post.title}</li>
        ))}
      </ul>
    );
  }

  // This function gets called at build time
- export async function getStaticProps() {
+ export const getStaticProps: GetStaticProps = async () => {
    // Call an external API endpoint to get posts
    const res = await fetch("http://localhost:1337/posts");
-   const posts = await res.json();
+   const posts: Post[] = await res.json();
  
    // By returning { props: { posts } }, the Blog component
    // will receive `posts` as a prop at build time
    return {
      props: {
        posts,
      },
    };
  }
  
  export default Blog;
```


まず TypeScript用の型を返してくれる関数があるっぽいのでimportする。

```diff_tsx:pages/blog.tsx
+ import { GetStaticProps } from 'next'
+ import { InferGetStaticPropsType } from 'next'
```

これはこんな感じでも書けそう。

```diff_tsx:pages/blog.tsx
+ import { GetStaticProps, InferGetStaticPropsType } from 'next'
```

次にPostのtype aliasesを定義している。

```diff_tsx:pages/blog.tsx
+ type Post = {
+   author: string
+   content: string
+   title: string
+ }
```

サンプルの中では `author` と `content` のみ書かれていたけど、 Simple Example で `<li>{post.title}</li>` とか書いて `title` を読んでたので今回は追記。

そして本丸の `InferGetStaticPropsType<typeof getStaticProps>)`。仮で`any`にしていたところにこの記述をすることで、良い感じに型を類推してくれる模様。

```diff_tsx:pages/blog.tsx
- function Blog({ posts }: {posts: any}) {
+ function Blog({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
```

上記でtype aliasesを定義したので、ここでmapするときのイテレータにも型を付けてあげる。

```diff_tsx:pages/blog.tsx
-       {posts.map((post: any) => (
+       {posts.map((post: Post) => (
```

`getStaticProps（）` も同様に下記のように修正。

```diff_tsx:pages/blog.tsx
- export async function getStaticProps() {
+ export const getStaticProps: GetStaticProps = async () => {
```

レスポンスもtype aliasesを使って明示的にをPostの配列として型付けする。

```diff_tsx:pages/blog.tsx
-   const posts = await res.json();
+   const posts: Post[] = await res.json();
```

こんな感じにすればこれまで通りに `/blog` が表示される。

```sh:command
$ open http://localhost:3000/blog
```

![スクリーンショット 2021-05-23 20.39.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/4b885a9c-8812-cb73-3647-53e969c7b9da.png)

最終的な `pages/blog.tsx` はこんな感じ。

```tsx:pages/blog.tsx
import { GetStaticProps } from 'next'
import { InferGetStaticPropsType } from 'next'

type Post = {
  author: string
  content: string
  title: string
}

function Blog({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
    <ul>
      {posts.map((post: Post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  );
}

// This function gets called at build time
export const getStaticProps: GetStaticProps = async () => {
  // Call an external API endpoint to get posts
  const res = await fetch("http://localhost:1337/posts");
  const posts: Post[] = await res.json();

  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts,
    },
  };
}

export default Blog;
```

次に Reading files: Use \`process.cwd()\` を試してみる。
ただこのまま作ると上記で作ったBlogとコンフリクトするので、下記のように修正を加える。

* `export`していた`Blog` を `ReadingFilesBlog` へ変更
* 参照先の記事が入っているフォルダを `posts` から `reading_files_posts` に変更

先に記事を適当に作っておく。
今回は `reading_files_posts` フォルダの下に `1.txt` と `2.txt` を適当に作る

```sh:command
$ mkdir reading_files_posts
$ echo 'Text No.1' > reading_files_posts/1.txt
$ echo 'Text No.2' > reading_files_posts/2.txt
```

次に例をTypeScriptにして、 `pages/reading_files_blog.tsx` というファイルに作っていく。

```tsx:pages/reading_files_blog.tsx
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { promises as fs } from 'fs'
import path from 'path'

type Post = {
  content: string
  filename: string
}

// posts will be populated at build time by getStaticProps()
function ReadingFilesBlog({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
    <ul>
      {posts.map((post: Post) => (
        <li>
          <h3>{post.filename}</h3>
          <p>{post.content}</p>
        </li>
      ))}
    </ul>
  )
}

// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries. See the "Technical details" section.
export const getStaticProps: GetStaticProps = async () => {
  const postsDirectory = path.join(process.cwd(), 'reading_files_posts')
  const filenames = await fs.readdir(postsDirectory)

  const posts = filenames.map(async (filename) => {
    const filePath = path.join(postsDirectory, filename)
    const fileContents = await fs.readFile(filePath, 'utf8')

    // Generally you would parse/transform the contents
    // For example you can transform markdown to HTML here

    return {
      filename,
      content: fileContents,
    }
  })
  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts: await Promise.all(posts),
    },
  }
}

export default ReadingFilesBlog
```

これをブラウザで表示してみる

```sh:command
$ open http://localhost:3000/reading_files_blog
```

![スクリーンショット 2021-05-30 22.02.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/cd265b80-e2ac-b363-0340-40faf6817122.png)


## \`getStaticPaths\` (Static Generation)

TypeScript: \`Use GetStaticPaths\` を見てみると、 `getStaticPaths()` も型をよしなにしてくれるのがありそうなので、 `pages/posts/[id].tsx` に適応してみる

```diff_tsx:pages/posts/[id].tsx
+ import { GetStaticProps } from 'next'
+ import { InferGetStaticPropsType } from 'next'
+ import { GetStaticPaths } from 'next'
+ 
+ type Post = {
+   id: number
+   author: string
+   content: string
+   title: string
+ }
+ 
- function Post({ post }) {
+ function Post({ post }: InferGetStaticPropsType<typeof getStaticProps>) {
    // Render post...
    return <h1>[{post.id}] {post.title}</h1>;
  }
 
  // This function gets called at build time 
- export async function getStaticPaths() {
+ export const getStaticPaths: GetStaticPaths = async () => {
  
    // Call an external API endpoint to get posts
    const res = await fetch("http://localhost:1337/posts");
-   const posts = await res.json();
+   const posts: Post[] = await res.json();
  
    // Get the paths we want to pre-render based on posts
-   const paths = posts.map((post) => ({
+   const paths = posts.map((post: Post) => ({
      params: { id: String(post.id) },
    }));
  
    // We'll pre-render only these paths at build time.
    // { fallback: false } means other routes should 404.
    return { paths, fallback: false };
  }
  
  // This also gets called at build time
- export async function getStaticProps({ params }) {
+ export const getStaticProps: GetStaticProps = async (context) => {
+   const { params } = context;
  
    // params contains the post `id`.
    // If the route is like /posts/1, then params.id is 1
    const res = await fetch(`http://localhost:1337/posts/${params.id}`);
    const post = await res.json();
  
    // Pass post data to the page via props
    return { props: { post } };
  }
  
  export default Post;
```

まずimportは `GetStaticProps` と同様に処理

```diff_tsx:pages/posts/[id].tsx
+ import { GetStaticProps } from 'next'
+ import { InferGetStaticPropsType } from 'next'
+ import { GetStaticPaths } from 'next'
```

type aliasesも。ただここではidも使ってるのでidを追加

```diff_tsx:pages/posts/[id].tsx
+ type Post = {
+   id: number
+   author: string
+   content: string
+   title: string
+ }
```

`InferGetStaticPropsType` も前回同様に処理

```diff_tsx:pages/posts/[id].tsx
- function Post({ post }) {
+ function Post({ post }: InferGetStaticPropsType<typeof getStaticProps>) {
```

`getStaticPaths()` については 例の通りに修正する。

```diff_tsx:pages/posts/[id].tsx
- export async function getStaticPaths() {
+ export const getStaticPaths: GetStaticPaths = async () => {
```

前回同様にレスポンスの型付けはPost[]に。

```diff_tsx:pages/posts/[id].tsx
-   const posts = await res.json();
+   const posts: Post[] = await res.json();
```

イテレータもPostに明示的に型付けする

```diff_tsx:pages/posts/[id].tsx
-   const paths = posts.map((post) => ({
+   const paths = posts.map((post: Post) => ({
```

`getStaticProps`は前回同様にしつつ、 `params` は `context`から取得するか足しで記述。

```diff_tsx:pages/posts/[id].tsx
- export async function getStaticProps({ params }) {
+ export const getStaticProps: GetStaticProps = async (context) => {
+   const { params } = context;
```

これをブラウザで表示してみる

```sh:command
$ open http://localhost:3000/posts/1
```

![スクリーンショット 2021-05-23 21.12.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/bf9ee343-b174-df25-4093-0db8c3d18a82.png)

最終的な `pages/posts/[id].tsx` はこんな感じ。

```tsx:pages/posts/[id].tsx
import { GetStaticProps } from 'next'
import { InferGetStaticPropsType } from 'next'
import { GetStaticPaths } from 'next'

type Post = {
  id: number
  author: string
  content: string
  title: string
}

function Post({ post }: InferGetStaticPropsType<typeof getStaticProps>) {
  // Render post...
  return <h1>[{post.id}] {post.title}</h1>;
}

// This function gets called at build time
export const getStaticPaths: GetStaticPaths = async () => {

  // Call an external API endpoint to get posts
  const res = await fetch("http://localhost:1337/posts");
  const posts: Post[] = await res.json();

  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post: Post) => ({
    params: { id: String(post.id) },
  }));

  // We'll pre-render only these paths at build time.
  // { fallback: false } means other routes should 404.
  return { paths, fallback: false };
}

// This also gets called at build time
// export async function getStaticProps({ params }: {params: any}) {

export const getStaticProps: GetStaticProps = async (context) => {
  const { params } = context;

  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 1
  const res = await fetch(`http://localhost:1337/posts/${params.id}`);
  const post = await res.json();

  // Pass post data to the page via props
  return { props: { post } };
}

export default Post;
```

## \`getServerSideProps\` (Server-side Rendering)

この章も Simple example と TypeScript: Use \`GetServerSideProps\` を合わせて作ってみる。

まずは聞きにいく先のAPIを作る必要があるので、strapiの管理画面を開く

```sh:command
$ http://localhost:1337/admin
```

![スクリーンショット 2021-05-30 22.50.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/19fbc0c8-4489-adee-6b26-09de039fc443.png)

サイドメニューから［Content-Type］→［Create new collection type］を選択する

![スクリーンショット 2021-05-30 22.56.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/9bf7a288-8bd9-410a-5ce6-a372ec3c3913.png)

次にコンテンツタイプ作成モーダルが開くので、［Display name］に`data`と入力する

![スクリーンショット 2021-05-30 22.57.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/c0660d30-0570-143b-e04b-34f125cd1a6d.png)

フィールド選択で［Text］を選択する

![スクリーンショット 2021-05-30 22.57.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/0f2ca6e9-994d-93fd-844d-1074ed8f0348.png)

［Name］に`name`と入力して［終了］をクリックする

![スクリーンショット 2021-05-30 22.58.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/11d33ffe-9070-a961-af7d-0b5cf04e5b2c.png)

定義したData型が表示されるので、［保存］をクリックしてリスタートされるまで待つ。

![スクリーンショット 2021-05-30 23.01.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/33cc000e-8fe1-3f61-f7f8-50dfe14c66f4.png)

ついでにデータもいくつか作成する。

サイドメニューに新しくできた［Data］をクリックし、表示されたページの［Dataを追加］ボタンをクリックする

![スクリーンショット 2021-05-30 23.03.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/5ba38e6b-628e-36be-fdea-f16a5bf9f466.png)

［Name］に`もけ`（なんでも良い）と入力して［保存］をクリックする

![スクリーンショット 2021-05-30 23.03.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/2c3668cb-ac7b-47fe-bb7f-3b6be2dc5725.png)

その後［Publish］してデータの準備は完了

![スクリーンショット 2021-05-30 23.04.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/48be6cf1-aef3-b42b-516e-c4dfc2ff831f.png)

次にAPIアクセスを可能にするために権限設定を行います。

サイドメニューの［設定］→［ロールと権限］と辿っていき、［Public］を選択します。

![スクリーンショット 2021-05-30 23.07.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/202564e5-7958-a7a7-cdea-d840412792ef.png)

［権限］フィールドの［DATA］の中にある［findone］と［find］にチェックを入れて、［Save］をクリックします。

![スクリーンショット 2021-05-30 23.08.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/20a619b5-4e6d-4ffb-2998-dddf3511a184.png)

これでstrapi側は完了。

これで http://localhost:1337/data にアクセスしたときに先ほど作成したデータが買えるようになりました。

```sh:command
$ curl http://localhost:1337/data
```
```json:output(整形済)
[
  {
    "id": 1,
    "name": "もけ",
    "published_at": "2021-05-30T14:05:08.147Z",
    "created_at": "2021-05-30T14:04:15.633Z",
    "updated_at": "2021-05-30T14:05:08.157Z"
  }
]
```

このままstrapiは起動したままにしておきます。


Simple example と TypeScript: Use \`GetServerSideProps\` を合わせて`pages/page.tsx`を作ります。

```tsx:pages/page.tsx
import { GetServerSideProps } from "next";
import { InferGetServerSidePropsType } from "next";

type Data = {
  name: string;
};

function Page({ data }: InferGetServerSidePropsType<typeof getServerSideProps>) {
  // Render data...
  return (
    <ul>
      {data.map((page: Data) => (
        <li>{page.name}</li>
      ))}
    </ul>
  );
  return <h1>{data.id}</h1>;
}

// This gets called on every request
export const getServerSideProps: GetServerSideProps = async (context) => {
  // Fetch data from external API
  const res = await fetch(`http://localhost:1337/data`);
  const data: Data[] = await res.json();

  // Pass data to the page via props
  return { props: { data } };
};

export default Page;
```

ポイントはgetServerSidePropsの時には`GetServerSideProps`と`InferGetServerSidePropsType`を使うところって感じかな・・・
確かに今回は`getStaticProps`がないので、`InferGetStaticPropsType`は使えなそう。

```tsx
import { GetServerSideProps } from "next";
import { InferGetServerSidePropsType } from "next";
```

これをブラウザで表示してみる

```sh:command
$ open http://localhost:3000/page
```

![スクリーンショット 2021-06-02 0.00.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/9bfed4dc-4d79-b6a5-ef3d-8ab06b7326b9.png)

## Fetching data on the client side

最後はクライアント側でデータを取得する例。SWRを使うらしい。
SWRはyarnでインストールすることができる。

```sh:command
$ yarn add swr
```
```txt:output
yarn add v1.22.10
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 2 new dependencies.
info Direct dependencies
└─ swr@0.5.6
info All dependencies
├─ dequal@2.0.2
└─ swr@0.5.6
✨  Done in 2.60s.
```


さっき作ったAPIを使う形で下記のように `pages/profile.tsx` を作っていきます。

```tsx:pages/profile.tsx
import useSWR from "swr";

function Profile() {
  const fetcher = (url: string) => fetch(url).then((res) => res.json());

  const { data, error } = useSWR("http://localhost:1337/data/1", fetcher);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

export default Profile;
```

TypeScriptにする時にいくつか修正しています。

1点目はfetcherの定義です。
JSON データを使用するAPIの場合はfetcheを定義する必要があるようなので、fetchをwrapする形で定義しています。

```tsx:pages/profile.tsx
  const fetcher = (url: string) => fetch(url).then((res) => res.json());
```

2点目はAPIのURLです。
ここではstrapiを叩く形で設定しています。

```tsx:pages/profile.tsx
  const { data, error } = useSWR("http://localhost:1337/data/1", fetcher);
```

最後は作ったProfile関数をexportする設定です。

```tsx:pages/profile.tsx
export default Profile;
```

ここまでしたらまたブラウザで表示してみる

```sh:command
$ open http://localhost:3000/profile
```

![スクリーンショット 2021-06-01 23.59.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164813/82f2b09d-a2a0-afb1-c248-316d992f0199.png)

なるほど、いろいろサクッとかけて便利そう。

ここまでで Basic Features / Data Fetching は完了になります。
次回は Built-in CSS Support を進めていきます。

# MacでNext.jsのGettingStartedを進めてみる シリーズの記事

1. [Getting Started をやった記事](https://qiita.com/tk_hamaguchi/items/5fde2017dc553f5a797e)
2. [Basic Features / Pages をやった記事](https://qiita.com/tk_hamaguchi/items/2e375ce4a3037c7e9c77)
3. Basic Features / Data Fetching をやった記事