【SSG】Django ＆ Next.jsでページネーションを分かりやすく

<h2>初めに</h2>
SSG（Static Site Generation）を使ってページネーションを超簡単に実装しますっっ！
最低限の説明にするために、関係のない実装（CSS,データベース、等）はしていません。

<h3>使う技術</h3>
<ol>
<ul>・Django-rest-frameworkのPaginator</ul>
<p>　　　　ページネーションのあるAPIを作ります。<p>
<ul>・Next.jsの静的ジェネレーター(getStaticPathsとgetStaticProps)</ul>
<p>　　　　Django側に対して、ページ番号を付けたURLへアクセスしてデータを取得します。</p>
</ol>

<h3>Django側のコード</h3>
私は、プロジェクト名prjの中にappとapiというアプリケーションを作成しました。
prj(プロジェクト名)
    app(アプリケーション名)
    api(アプリケーション名)
<h4>プロジェクトの設定</h4>

```python:settings.py
INSTALLED_APPASにrest_framework,app,apiを追加するだけでOK
データベースも標準のSQLiteを使っちゃってますが、PostgreSQLとかでも同じことです。
各々の設定はお任せで。
```

```python:prj/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),

    path('api/', include('api.urls')),# ←追加
]
```

<h4>appアプリケーション</h4>

```python:models.py
from django.db import models

class Datas(models.Model):
    title = models.CharField(max_length=100)
    content = models.CharField(max_length=1000)

    def __str__(self):
        return self.title
```
```python:admin.py
from django.contrib import admin
from app.models import Datas

admin.site.register(Datas)
```

<h4>apiアプリケーション</h4>

```python:urls.py
from django.urls import path
from . import views

from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'data', views.DataViewSet, basename='data')

urlpatterns = router.urls

urlpatterns = [
    path('data/', views.DataViewSet.as_view({'get': 'list'})),
    path('data/pagination/', views.DataPaginationView.as_view({'get': 'list'}))
]
```


```python:views.py
from rest_framework import viewsets
from app.models import Datas
from .serializers import DataSerializer
from .pagination import CustomPagiNumberPagination

class DataViewSet(viewsets.ModelViewSet):
    model = Datas
    serializer_class = DataSerializer
    queryset = Datas.objects.all().order_by('id')

class DataPaginationView(viewsets.ModelViewSet):
    model = Datas
    pagination_class = CustomPagiNumberPagination
    serializer_class = DataSerializer
    queryset = Datas.objects.all().order_by('id')
```
```python:serializers.py（新規作成）
from app.models import Datas
from rest_framework import serializers

class DataSerializer(serializers.ModelSerializer):
    class Meta:
        model = Datas
        fields = '__all__'
```
シリアライザーはこれでOK。
```python:pagination.py(新規作成)
from rest_framework import pagination

class CustomPagiNumberPagination(pagination.PageNumberPagination):
  page_size = 5# １ページのデータ数を指定します。
  page_size_query_param = 'count'
  max_page_size = 5
  page_query_param = 'p'# api/data/pagination/?p=2 などとできます。
```

<h4>Django動作確認</h4>

http://127.0.0.1:8000/admin/
にアクセスし、Datasのデータを１０個くらい追加しましょう。
今は、pagination.pyでデータ数を５に設定しています。

http://127.0.0.1:8000/api/data/pagination/?p=1
にアクセス↓
![スクリーンショット (697).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522774/d58d118c-9efb-4a84-7f02-822050a02385.png)
こんな感じで表示されれば成功です。

<h3>Next.js側のコード</h3>
<h4>Create Next App</h4>
任意の名前でアプリケーションを作成します。

```javascript:コマンド
npx create-next-app@latest
```
元からあるpagesフォルダ内にdatas/page/[id].jsを作りました。
また、pagesと同じ階層に、lib/datas/datas.jsを作りました。

<h4>npm(もしくはyarn)でインストールするもの</h4>

以下の、dependencies内のモジュールを集めてきてください！
バージョンも合わせるとエラーを回避できます。

ちなみに、Paginationのスタイルと機能は、Material-uiにお任せしています。
```javascript:package.json
{
  "name": "next-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@emotion/react": "^11.9.0",
    "@emotion/styled": "^11.8.1",
    "@mui/material": "^5.8.6",
    "next": "12.2.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  },
  "devDependencies": {
    "eslint": "8.18.0",
    "eslint-config-next": "12.2.0"
  }
}

```
<h4>データ取得をする2つの関数を定義します</h4>

```javascript:lib/datas/datas.js
export async function getAllDatasId() {
  const res = await fetch(new URL(`http://127.0.0.1:8000/api/data/pagination/`));
  const data = await res.json();
  const totalPages = Math.ceil(Number(data.count)/Number(10))//6
  let paramId = []
  for (let i = 0; i < totalPages; i++) {
    const parameter = {
      params: {
        id:String(i+1)
      },
    };
    paramId.push(parameter);
  }
  return paramId
}
export async function getData(page) {
  const res = await fetch(new URL(`http://127.0.0.1:8000/api/data/pagination/?p=${page}`));
  const datas = await res.json();
  return datas;
}
```
getAllDatasId()はgetStaticPaths(ページネーションの動的なページ番号を取得します)のための関数です。django-rest-frameworkで用意したAPIのcountから求めています。
getData()はgetStaticPropsで使います。getStaticPropsから求めたページ番号を基に、データを取得します。
ぜひ、コンソールに出したりして確かめてみて下さい！

```javascript:datas/page/[id].js
import { useEffect, useState } from 'react';
import { Pagination } from '@mui/material';
import { useRouter } from 'next/router';
import { getAllDatasId, getData } from '../../../lib/datas/datas';

export default function All(props) {
  const { datas } = props;
  const [page, setPage] = useState(1),//ページ番号
        [count, setCount] = useState();//総ページ数
  const router = useRouter();

  const clickPage = (e, page) => {
    setPage(page);
    router.push(`${page}`)
  }
  useEffect(() => {
    setCount(Math.ceil(Number(datas && datas.count && datas.count)/Number(10)))
  },[count,datas])

  return (
      <div>
        <div style={{marginTop: "50px", textAlign: "center"}}>
          <Pagination
            count={count}//総ページ数
            color="primary"
            variant="outlined"
            onChange={clickPage}//第2引数にページ番号が入る
            page={page}
          />
          {
            datas.results.map(data => {
              return (
                <div>
                  <hr/>
                  <p>{data.title}</p>
                  <p>{data.content}</p>
                  <hr/>
                </div>
              )
            })
          }
        </div>
      </div>
  )
}

export async function getStaticPaths() {
  const paths = await getAllDatasId();
  return {
    paths,
    fallback:true// ないページ番号にアクセスがあった際、falseの場合は404を返します。
                  // trueなら、同様に静的なページが表示されますが、propsが空になります。
  };
}
export async function getStaticProps({ params }) {
  const datas = await getData(params.id)
  return {
    props: {
      datas,
    },
    revalidate:100// 本番環境の時に100秒毎にデータを読み込みます。
  }
}
```
getStaticPathsでページ番号を取得し、getStaticPropsでページ毎のデータを取得できました。
Next.jsでは、SPAとしてURLを変更する際、useRouter()オブジェクトのrouter.push(相対パス)を使います。
clickPageという自作の関数は、Paginationのボタンがクリックされた時に、pageのステートを更新し、router.push()でURLに反映させるという処理をします。
こんな感じでuseStateで管理しているpageを[id].jsのidの部分と連動させています。

useEffect内では、Paginationに使う、ページ番号の総数を求めています。
３ページあるなら３が入る事で、Paginationが1,2,3と表示されます。


<h4>簡単な説明↓</h4>

・getStaticPathsで得られるもの
データの総数÷１ページのデータ数でページ数を求めて下のgetStaticPropsのためのページ番号をすべて準備しています。
```json
[ 
    {
    params: {
        id: '1'
        } 
    }, 
    {
        params: {
            id: '2' 
        }
    } 
]
```
・getStaticPropsでえられるもの
上のgetStaticPathsから引数の{params}でページ番号を取得して、ページ毎にデータを取得しています。
```json
{
  count: 11,
  next: 'http://127.0.0.1:8000/api/data/pagination/?p=2',
  previous: null,
  results: [
    {
      id: 1,
      title: 'タイトルです',
      content: '内容です。',
      savedat: '2022-06-30T18:25:53.505316+09:00'
    },
    {
      id: 2,
      title: 'タイトル２です',
      content: '内容２です。',
      savedat: '2022-06-30T18:26:04.498775+09:00'
    },
    {
      id: 3,
      title: 'タイトル３です',
      content: '内容３です。',
      savedat: '2022-06-30T18:26:15.109920+09:00'
    },
    {
      id: 4,
      title: 'タイトル４です',
      content: '内容４です。',
      savedat: '2022-06-30T18:26:28.785373+09:00'
    },
    {
      id: 5,
      title: 'タイトル５です',
      content: '内容５です。',
      savedat: '2022-06-30T18:26:43.141520+09:00'
    }
  ]
}
```

<h4>動作確認</h4>

![スクリーンショット (699).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522774/0ee97324-0ed2-9929-dbfc-f8911e8e6a92.png)


<h2>おわり</h2>
DjangoバックエンドのNext.jsの静的サイトジェネレーションを使った時のページネーションの作り方をまとめました。
DjangoでページネーションのAPIを作ってNext.jsで受け取ればいいという事でした。
<br/>
<br/>
<strong>Django側での実装↓</strong>
<ol>
<li>django-rest-frameworkのpaginatorを使ってpaginationのあるAPIを作る</li>
<li>urls.pyで外部からアクセスできるようにする</li>
</ol>

<strong>Next.js側での実装↓</strong>
<ol>
<li>getStaticPaths</li>
<p>djangoのAPI上のcount（総データ数）からpage数を計算する。<br/>count/１pageのデータ数=page数</p>
<p></p>
<li>getStaticPropsでデータを取得</li>
<p>getStaticPathsからpage数を受け取ってDjangoのAPIにアクセスしてデータを取得できます。</p>
</ol>

以上です。
見て頂きありがとうございました！
質問や指摘等あれば気軽によろしくお願いします！