react入門9 with Nextjs

プロジェクトの作成

sample-appというプロジェクトを作る際には

npx create-next-app sample-app --use-npm --ts

npm はnode package manager、つまりパッケージの管理ツールです。
npx はnode package executer、つまりパッケージの実行を行うツールです。すでにnextはインストールしているので、npxで実行しています。

コマンド詳細は下記です。主なオプションとしては --ts(タイプスクリプト利用),--example "url" (urlテンプレダウンロード)です。

脱線ですが、パッケージ管理ツールは、npm(マイクロソフト)とyarn(facebook)、どちらでも良いと思います。react自体facebookのため、nextもyarn 推しのようです。ただ、マイナーライブラリを検索すると公式ツールであるnpmで引っかかることが多い印象です。--use-npmオプションで、npmでも快適?に使えるようようです。

また--tsオプションは今後つけていこうと思います。タイプスクリプトについては下記がまとまっています。

目的

公式チュートリアルでは、サーバーサイドレンダリングの例として、自分のディレクトリにあるmdファイルを読み込んでいましたが、別の例としてjsonplaceholderのJSONオブジェクトを読み込むように変更して復習します。公式チュートリアルにざっと目を通していることを前提としています。
作成するサンプルの仕様、設計は以下です。

  1. トップページにはイラストがある(next/imageを利用)

  2. 別の静的ページがある(pagesディレクトリの理解)

  3. 各ページはリンクする(next/linkを利用)

  4. 各ページは独自のタイトルをもつ(next/headを使用)

  5. RESTAPIで取得したToDo毎のページを動的につくる(getStaticPaths,getStaticPropsを利用)

  6. 装飾はしない(今後MUI導入やnext/layoutsの記事を書く予定)

単純な状態からスタート

sample-appのディレクトリを次のようにします。public内のファイルを削除します。pages内のapiフォルダ、__appを削除します。typesフォルダを削除します。index.tsxを以下で置き換えます。本当にまっさらです。関数コンポーネントに相当する関数がNext Pageで型定義されています。

この状態でnpm run dev すると、ブラウザに表示されます。開発者ツールで見ると、勝手にHTMLが生成されています。create-react-appでは、index.htmlくらい書いていたのに、今は裏でやってくれています。

変換についてカスタマイズしたい場合は、nextjsが決めているdocumentコンポーネントを上書きする方法があります。またその上にはappコンポーネントが存在します。


1. next/image

トップページに画像を貼り付けます。まずpublicフォルダ内にimagesフォルダを作成し、その中に適当な画像test.pngを格納し、下記のようにindex.tsxにImageコンポーネントを変更します。

import type { NextPage } from 'next'
import Image from 'next/image'

const Home: NextPage = () => {
  return (
    <>
        <Image 
          src="/images/test.png"
          alt="test img"
          width={100}
          height={100}
        />
        <p>hello</p>
    </>
  )

}

export default Home

2.静的なページを追加

pagesディレクトリにstatic-page.tsxファイルを追加します。

import { NextPage } from "next";

const StaticPage:NextPage = () =>{
    return(
        <p>静的な別のページ</p>
    )
}
export default StaticPage

ファイル名をURLに付加する形でアクセスすると表示されます。

pagesフォルダ外にstatic-page.tsxを移動すると、404エラーとなります。

3.next/link

トップページから静的ページへ、その逆へリンクを貼るには、次のようにします。</>の直前にLinkタグを入れていいます。hrefはホームを”/”とします。また<Link>タグ内に<a>タグの中に文字などリンクになる要素を指定します。

//index.tsx

import type { NextPage } from 'next'
import Image from 'next/image'
import Link from 'next/link'

const Home: NextPage = () => {
  return (
    <>
        <Image .....
        />
        <p>hello</p>
        <Link href="/static-page"><a>別ページへ</a></Link>
    </>
  )

}

export default Home
//static-page.tsx

import { NextPage } from "next";
import Link from "next/link";
const StaticPage:NextPage = () =>{
    return(
        <>
            <p>静的な別のページ</p>
            <Link href="/"><a>ホームへ</a></Link>
        </>

    )
}
export default StaticPage

4.next/head

各ページは、それぞれのHeadタグをもち、その中にページのタイトルなどを個別にもつことができます。ここでは下記のようにそれぞれのページのタイトルを追加しています。「Headに何を書くべきか」は、ググってみてください。Nextjsでデフォルトで適用される、HTML変換のベースになっているdocumentコンポーネント、全てのコンポーネントに適用されるAppコンポーネントもHeadを持っていますので、ユーザーはアプリケーションドメインに特化した内容を記載することになります。

//index.tsx
...
import Head from 'next/head'

const Home: NextPage = () => {
  return (
    <>
      <Head><title>ホーム</title></Head>
      ....
    </>
  )
}

export default Home
//static-page.tsx
・・・
import Head from "next/head";

const StaticPage:NextPage = () =>{
    return(
        <>
            <Head><title>別ページ</title></Head>
            <p>静的な別のページ</p>
            <Link href="/"><a>ホームへ</a></Link>
        </>
    )
}
export default StaticPage

5  REST APIで取得したToDo毎のページを動的につくる

全体の流れを確認しておきます。

  • ページを動的につくるには、pageディレクトリ配下に[id].js(or tsx)を配置します。

  • [id].jsの中では、任意の idが取りうる範囲のリストを返す、getStaticPathsという関数を定義する必要があります。

  • [id].jsの中では、任意の idを引数として、データをpropsとして返す、getStaticPropsという関数を定義する必要があります。

  • [id].jsの中では、getStaticPropsを受け取りコンテンツを表示するリアクトコンポーネントが(当然)必要です。

  • ブラウザからpageディレクトリ配下の配置に従いURLを入力すると、Nextjsがページを生成してれます。

5.1 番外:typescriptでgetStaticPropsを定義する練習

まずは、練習としてトップページにをgetStaticPropsで読み込んだpropsを表示してみます。一旦現在のindex.tsxのバックアアップをとり、新規にindex.tsxでgetStaticPropsを定義し、JSONデータを読み込み、Homeコンポーネントに渡して動作を確認します。

//index.tsx
import type { NextPage } from 'next'
import { InferGetStaticPropsType } from 'next'

type Props = InferGetStaticPropsType<typeof getStaticProps>;//キモ

type people = {
  id:number;
  name:string;
}
const Home: NextPage<Props> = ({data}) => {
  return (
    <>
      <p>id:{data.id}</p>
      <p>name:{data.name}</p>
    </>
  )
}

export const getStaticProps = async () => {

  const data:people ={
    id:1,
    name:"tom"
  }

  return{
    props:{
      data,
    },
    revalidate:1
  }
}

export default Home

typescriptを使っているのでgetStaticPropsが返すpropsの中身peopleの型を定義する必要があります。またgetStaticPropsはasync関数なので、返すpropsはpromiseですが、InferGetStaticPropsTypeというツールで意識せずにすみます。ただ、公式サイト通りにやってpropsの型推論がうまくいかず、下記が参考になりました。


5.2 pages配下に動的ページのテンプレートを定義する

page/todoフォルダを生成し、[todoId].tsxを配置しました。

// /pages/todo/[todoId].tsx

import { NextPage } from "next";
import { InferGetStaticPropsType, GetStaticPropsContext } from "next";

type Props = InferGetStaticPropsType<typeof getStaticProps>;

const todoPage:NextPage<Props> = ({data})=>{

    return(
        <>
        </>
    )

}

export const getStaticProps = async ({params}:GetStaticPropsContext)=>
{
    const data = {}//params.todoIdを引数としてデータを受け取る

    return{
        props:{
          data,
        },
        revalidate:1
      }
}

export const getStaticPaths = async () =>
{
    const paths = {} //pathの型は{params{todoId:string}}
    return {
      paths,
      fallback: false
    }
}

export default todoPage

重要な点は、下記です。

  • [ページ名].tsxのページ名がgetStaticPropsが受け取るpramsのid変数として設定されていること。ここではgetStaticPropsにはtodoIdが含まれ、todoIdがページ名になる。

  • getStaticPropsはparams.todoIdを受け取り、データを選択的に取得できる。

  • params.todoIdはgetStaticPathsが返すリストに含まれること

  • URLが文字列であるためparams.todoIdはString文字列であること。

  • /pages/todo/"params.todoId"が取りうる文字列のどれか”でアクセスすると、param.todoIdにその文字列が設定され、getStaticPropsに渡る。

  • getStaticPropsの引数には{params}:GetStaticPropsContextを記載する。

getStaticPathsは指定されたURLが有効なものかを判定するツールとも言えます。

5.3 動的ページのgetStaticPaths,getStaticPropsの中身(ロジック)を別のファイルに書く

今回もjsonplaceholderを使用させてもらいます。jsonplaceholderのtodoは1から199までの数字を最後に付けると数字に応じたJSONを返します。その型は以下のTodoDataです。
以下のコードを、新たに作成したdomain/interfaceフォルダ内に配置しました。JSXを使っていないので、ただのts拡張子です。

// /domain/interface/getTodoJson.ts

export type TodoData = {
    userId:number,
    id:number,
    title:string,
    completed:boolean,
}

export type Path = {
    params:{
        todoId:string
    }
}


export class GetTodoJson{
    
    base_url:string

    constructor(){
        this.base_url = "https://jsonplaceholder.typicode.com/todos/"
    }

    async getData(num:number){
        const response = await fetch(this.base_url + String(num))
        const data = await response.json()
        return data
    }
}

export const getPathList=()=>{

    const num1to199:number[] = [...Array(199)].map((_,i)=> i+1) 
    const pathlist:Path[] = num1to199.map((num,idx) =>{
        return {
            params:{
                todoId: num.toString()
            }
        }
    })
   
    return pathlist
}


getPathListは、Path型のオブジェクトのリストで、1から199までの数字を文字としてtodoIdに格納します。
GetTodoJsonクラスは、数字を引数として受けとりTodoData型のJSONを返すgetData関数を提供します。

5.4 動的ページから上記のファイルが提供する中身をimportして、動的ページを完成させる。

先ほどのテンプレートに、上記の関数などを使って動的ページを完成させます。

// /pages/todo/[todo-id].tsx


import { NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import { InferGetStaticPropsType, GetStaticPropsContext } from "next";

import {GetTodoJson,TodoData,getPathList} from '../../domain/interface/getTodoJson'


type Props = InferGetStaticPropsType<typeof getStaticProps>;

const todoPage:NextPage<Props> = ({data,id})=>{

    return(
        <>
            <Head><title>{id}番目</title></Head>
            <p>user id: {data.userId}</p>
            <p>task id: {data.id}</p>
            <p>task title: {data.title}</p>
            {
                data.completed
                ? <p>タスク完了</p>
                : <p>未完了</p>
            }
            <Link href="/"><a>ホームへ</a></Link>
        </>
    )

}

まず、表示のためのコンポーネントですが、受け取ったdata,idを上記のように表示しているだけです。

export const getStaticProps = async ({params}:GetStaticPropsContext)=>
{
    let id:string
    if (params === undefined){
        id = "0"
    }else{
        id = params.todoId as string
    }

    const getTodo = new GetTodoJson()
    const num = parseInt(id)
    const data:TodoData = await getTodo.getData(num)//1 to 200

    return{
        props:{
          data,
          id
        },
        revalidate:1
      }
}

getStaticPropsへのGetTodoJsonの組み込みですが、paramsがundefinedの時があるとタイプスクリプトから怒られたので、最初にif文で受け取っています。また、データだけではなく、idもpropsに詰めています。

export const getStaticPaths = async () =>
{
    const paths = getPathList()
    return {
      paths,
      fallback: false
    }
}

export default todoPage

getStaticPathsはgetPathListを呼び出しpathsとして設定しているだけです。


5.5 トップページからそれぞれの動的ページにジャンプするリンクを貼る。

トップページを以下のように改造しました。

//index.tsx
import { useState } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import Link from 'next/link'


const Home: NextPage = () => {
  const [number,setNumber] = useState(1)

  const handler_numChange = (e: React.ChangeEvent<HTMLInputElement>) =>{
    setNumber(parseInt(e.target.value))
  }

  return (
    <>
        <Head><title>ホーム</title></Head>
        <Image 
          src="/images/test.png"
          alt="test img"
          width={100}
          height={100}
        />
        <p>hello</p>
        <Link href="/static-page"><a>別ページへ</a></Link><br/>
        
        <label>
          1から199を半角入力してください:<br/>
          <input type="number" value={number} onChange={handler_numChange}/>
        </label><br/>
        <Link href={`/todo/${number.toString()}`}><button>todoへ</button></Link>
        
    </>
  )
}

export default Home

次回

みんな大好き装飾について、人気の高いMUIの使い方について記載します。

この記事が気に入ったらサポートをしてみませんか?