Next.js version 13の新機能の補足解説

Next.js v13ではいくつかの大きな変更が加えられました。本書執筆時はNext.js v13リリースの直後であり、新しく追加された機能がどこまで広く使われるのか未知数でしたが、v13リリースから半年ほど経ち、いくつかの機能、特にappフォルダは定着の兆しがあるので、ここで本書の内容とは切り離した形で補足します(*なお現時点ではappフォルダは依然として「experimental」で本番環境での使用は推奨されていません)。

(*5月5日追記:5月4日にリリースされたNext.js v13.4よりstable/安定版となりました)

以下、目次です。


― v13のインストール

― appフォルダ

― Layoutコンポーネント

― getStaticPaths/getStaticPropsとgenerateStaticParams


v13のインストール

最新版のNext.jsのインストールから始めましょう。ターミナルを開いて任意のフォルダへ移動し、次のコマンドでNext.jsをインストールしてください。

npx create-next-app@latest nextjs13-app-test

最後の@latestによって、その時点での最新版がインストールされます。フォルダ名はここではnextjs13-app-testとしています。

インストール開始前に次のような質問が出ます(2023年5月現在)。

? Would you like to use TypeScript with this project? … No / Yes
? Would you like to use ESLint with this project? … No / Yes
? Would you like to use Tailwind CSS with this project? … No / Yes
? Would you like to use `src/` directory with this project? … No / Yes
? Use App Router (recommended)? › No / Yes
? Would you like to customize the default import alias? › No / Yes

「Yes」か「No」を矢印キーで選択して「Enter」で決定します。TypeScript、ESLint、Tailwind CSS、src directory利用の質問には「No」を選択してください。そして「Use App Router (recommended)」の質問には「Yes」を選ぶことで、v13の新機能appフォルダが利用できるようになります。最後の「customize the default import alias」は今回重要ではないので「No」を押して進んでください。

インストールが完了したらVSコードで開きましょう。package.jsonを見ると、"next": "13.x.x"とあり、Next.jsのversion 13がインストールされているのがわかります(執筆時点では"next": "13.3.0")。

appフォルダ

v13ではフォルダ構成の方法に根本的な変更が加えられました。なお現時点(2023年4月)ではこの機能はまだ「experimental(実験段階)」ですが、今後はこれがデフォルトになっていくと考えられます。

まずnext.config.jsを見てください。

// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
}

module.exports = nextConfig

experimentalという文字が見え、appDirつまり「app Directory(appフォルダ)」機能が追加されているのがわかります。

次にフォルダ構成を見ると、最初に気がつくのはappフォルダのあることです。

pic1.jpg

従来まではpagesフォルダ内にabout.jscontact.jsといったような各ページを作ってきましたが、appフォルダでは各ページ毎にひとつフォルダを作り、その中にpage.jsというファイルを作る構成となります。

表にすると以下のようになりますが、実際に試した方がわかりやすいのでコードを見ながら解説していきます。

URL 従来 v13
/ /pages/index.js /app/page.js
/about /pages/about.js /app/about/page.js
/contact /pages/contact.js /app/contact/page.js

まずapp/page.jsを見てください。これは従来のpages/index.jsにあたります。

最初に/aboutページを作ってみましょう。appフォルダ内にaboutというフォルダを作ります。

pic2.jpg

この中にファイルpage.js作ります。

pic3.jpg

そこに次のコードを打ってください。普通のReactのコードです。

// app/about/page.js

const About = () => {
    return (
        <h1>Aboutページ</h1>
    )
}

export default About

保存し、Next.jsを次のコマンドで起動しましょう。

npm run dev

http://localhost:3000/aboutにアクセスするとAboutページが開きます(CSSスタイルとしてglobals.cssが当たっています)。

同じ要領でコンタクトページを作ってみましょう。まずcontactフォルダをappフォルダ内に作ります。

pic4.jpg

その中にpage.jsファイルを作りましょう。

pic5.jpg

次のコードを打ちます。

// app/contact/page.js

const Contact = () => {
    return (
        <h1>Contactページ</h1>
    )
}

export default Contact

保存して、http://localhost:3000/contactにアクセスするとContactページができているのがわかります。

ここまでで、従来のようにファイル名ではなく、フォルダ名がページのURLと対応しているのがわかりました。本書のブログ記事ページで使った特殊なファイル、[slug].jsも発想としては同じです。[slug]という名前をファイルではなくフォルダに使います。実際に確認してみましょう。

まずblogフォルダを作り、その中にpage.jsを作ります。

pic6.jpg

そこに次のコードを打てば/blogページができます。

// app/blog/page.js

const Blog = () => {
    return (
        <h1>Blogページ</h1>
    )
}

export default Blog

blogフォルダの中に[slug]フォルダを作ってください。

pic7.jpg

その中にpage.jsファイルを作ります。

pic8.jpg

そこに次のコードを打ちます。

// app/blog/[slug]/page.js

const SingleBlog = () => {
    return (
        <h1>SingleBlogページ</h1>
    )
}

export default SingleBlog

保存します。URL欄のhttp://localhost:3001/blog//blog/以下にどの文字列をつなげても、いま作ったページが表示されるようになります。たとえばhttp://localhost:3001/blog/aabbcchttp://localhost:3001/blog/xxyyzzなどです。

なおこのページでURLのパラメータを取得するには次のようにします。

// app/blog/[slug]/page.js

const SingleBlog = (props) => {     // 追加
    console.log(props.params.slug)     // 追加
    return (
        <h1>SingleBlogページ</h1>
    )
}

export default SingleBlog

propsに含まれたparams内部のslugで、URLのパラメーターを取得できます。なおここでslugとなっているのはフォルダ名が[slug]であるからで、もしフォルダ名が[id][abc]の場合、それぞれprops.params.idprops.params.abcとなります。

Layoutコンポーネント

従来までサイト全体に適用したいスタイルなどは_app.jsに書き加えていましたが、v13ではappフォルダ内にlayout.jsというファイルを作ると、それがサイト全体に適用されます。

現在appフォルダを見ると、すでにlayout.jsがあり、これがサイト全体で適用されています。本当に適用されているのか確認したいので、次のように余分なタグを書き加えてみましょう。

// app/layout.js

import './globals.css'

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body><h1>テスト</h1>{children}</body>     // 追加
    </html>
  )
}

保存して、/about/contactなどにアクセスすると、「テスト」という文字が表示され、たしかにlayout.jsファイルが全ページに適用されているのがわかります。

しかし一部のページでは別のレイアウトを適用したいというケースもあります。その場合はどうすればいいのでしょうか。

実はページ毎に作ったフォルダの中にlayout.jsを作ると、それがフォルダ内のすべてのページに適用されます。試してみましょう。

blogフォルダ内にlayout.jsを作ります。

pic9.jpg

次のコードを打ちます。

// app/blog/layout.js

export default function BlogLayout({ children }) {
    return (
        <body>
            <div>
                <h1>ブログページレイアウト</h1>{children}
            </div>
        </body>
    )
}

保存してhttp://localhost:3000/blogを開くと、app/layout.jsのレイアウトではなく、いま作ったapp/blog/layout.jsが適用されているのがわかります

http://localhost:3000/blog/xyzを開いてもそれが適用されているので、/blog/以下のページにはすべてblogフォルダ内のlayout.jsが適用されているのがわかります。

getStaticPaths/getStaticPropsとgenerateStaticParams

appフォルダにはReact Server Componentsという仕組みが使われているため、appフォルダ内ではgetStaticPathsgetStaticPropsが使えなくなりました。appフォルダ内で動的データをあつかう新しいコードの書き方の例を紹介します。

まず準備として、dataフォルダとutilsフォルダを本書完成見本コードよりダウンロードし、appフォルダ内に配置してください。


完成見本コード: https://github.com/mod728/nextjs-book-portfolio-site

pic10.jpg

次にマークダウンファイルを扱うためのパッケージをインストールしましょう。

npm install raw-loader gray-matter

そしてnext.config.jsraw-loaderを使うコードを書き加えます。

// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
  // ↓追加
  webpack: function (config) {
    config.module.rules.push({
        test: /\.md$/,
        use: "raw-loader",
    })
    return config
  },
  // ↑追加
}

module.exports = nextConfig

以上で準備が完了です。


getStaticPathsに変わるものとして導入されたのがgenerateStaticParamsです。次のように書きます。

// app/blog/[slug]/page.js

import { getAllBlogs } from "../../utils/mdQueries"

const SingleBlog = (props) => {
    return (
        <h1>SingleBlogページ</h1>
    )
}

export default SingleBlog

export async function generateStaticParams() {
    const { orderedBlogs } = await getAllBlogs()
    const paths = orderedBlogs.map((orderedBlog) => `/${orderedBlog.slug}`)
    return paths
}

URLの生成・登録に使われるgetStaticPathsに対し、ページで表示するデータ取得のために使っていたのがgetStaticPropsです。appフォルダ内ではgetStaticPropsは廃止され、普通のfetch()でデータ取得ができるようになっています。コード例として次のようになります。なお= (props) =>asyncを書き忘れないようにしましょう。

// app/blog/[slug]/page.js

import { getAllBlogs, getSingleBlog } from "../../utils/mdQueries"

const SingleBlog = async(props) => {
    const { singleDocument } = await getSingleBlog(props)
    console.log(singleDocument)
    return (
        <h1>SingleBlogページ</h1>
    )
}

export default SingleBlog

export async function generateStaticParams() {
    const { orderedBlogs } = await getAllBlogs()
    const paths = orderedBlogs.map((orderedBlog) => `/${orderedBlog.slug}`)
    return paths
}