はじめに
next.jsをお勉強することになったので、アウトプットもかねて書きます。
間違い等あったらご指摘いただけますと幸いです。
書くこと
各種環境
npm
10.5.0
node
v21.7.2
react
^18.3.1
next
^14.2.3
next.jsってなんやねん
・公式ドキュメント
What is Next.js?
Next.js is a React framework for building full-stack web applications. You use React Components to build user interfaces, and Next.js for additional features and optimizations.Under the hood, Next.js also abstracts and automatically configures tooling needed for React, like bundling, compiling, and more. This allows you to focus on building your application instead of spending time with configuration.
Whether you're an individual developer or part of a larger team, Next.js can help you build interactive, dynamic, and fast React applications.
(ガバ翻訳)
next.jsはフルスタックwebアプリケーション作成のための、Reactフレームワークです。Reactを使ってインターフェースを作成し、Next.jsを使って機能の追加や最適化ができます。
内部的には、Reactに必要なバンドル、コンパイルなどのツールを自動で抽象化して設定します。これにより、余計な設定に気を取られることなく、アプリケーションの作成に集中することができます。
個人開発でも、チームの大規模開発でも、Next.jsはインタラクティブ(双方向的)で、動的で、高速なReactアプリ開発を助けてくれるでしょう。
フレームワーク or ライブラリ ???
reactはライブラリで、next.jsはフレームワーク。違いはなんだろう?
・ライブラリ
ライブラリは使用者に対して開発に便利な関数をまとめて提供してくれるもの。細かいパーツごとに使用できるため、自由度が高い。
・フレームワーク
フレームワークは、実装に必要な機能なんかがある程度まとまっていて、決められた枠組みに沿って開発できるもの。自由度は低め。
=>
ライブラリはライブラリ使用者が「どう使うか」という支配権を持つのに対し、
フレームワークはフレームワーク側が「どう使わせるか」という支配権を持つ。
引用: https://ssaits.jp/promapedia/technology/library-framework.html
・特徴
・SSG(Static Site Generation)、SSR(Server-Side Rendering)、CSR(Client-Side Rendering)を統合的に扱える!
SSG (Static Site Generation)
ビルド時に、静的なページを作っておいて、リクエストを受けたらその静的なファイルを渡す手法。メリット
・クライアント側でjsのコンパイルなどがないため、軽いのと、完全な状態のhtmlを渡すので、SEO的に対策がしやすい。デメリット
・更新作業をするときは、毎回ビルドしなきゃいけない。
SSR(Server-Side Rendering)
クライアントからリクエストを受けた時、サーバー側でjsを実行して、htmlを生成し、それを渡す手法。メリット
・サーバーで実行するため、クライアントのマシンスペックに縛られない表示速度を保てる上、初回レンダリングも早い。デメリット
・サーバースペックが求められる。
・毎回更新時にサーバーとの通信が入る。
CSR(Client-Side Rendering)
クライアントからリクエストを受けた時、クライアント側でjsを実行して、htmlを生成し、domに配置、表示させる方法。reactなどはデフォでこれ。メリット
・動的な変更に強い?デメリット
・クライアント側でjsを実行するのでマシンスペックが必要だったり、初回レンダリングが遅くなったりする。
・htmlが最低限の記述の状態でクライアント側に渡されるので、クローラーが何のサイトか判別できず、SEO対策が超貧弱になる。
要するに、
- SSGはもはや公開前にビルドしておいて静的なページを作っておき、それを配信すること。
- SSRは公開前に静的なページは生成しないけど、公開後にサーバー側でレンダリングして、その結果をHTMLで渡すから、完全なhtmlとかSEO対策のタグとかを織り込むことができて大変良い。
- CSRは、渡すときはJSの形で渡しておいて、クライアントサイドでコンパイル、DOMに配置するから、SEO対策がし辛く、かつ初回読み込みがコンパイルのせいで遅くなりがち、ということ。
next.jsはこの3つに加え、ISR(SSGの定期更新版)の4つの公開方法を、ページごとに設定することができるっぽい。
すなわち、サイトの説明だけを載せるようなページでは、頻繁に更新は要らないから、SSGで設定してあげて、ユーザーの情報を取得するようなページでは、頻繁に更新したいから、ISRとかSSRとかを使いたいってコト?
・next.jsを用いることの利点
上記記事がとても参考になりました。
特に、
・ファイルベースルーティング機能
=> 特別にルーティングを設定しなくても、/**Page Router 📁page L name.tsx => localhost:3000/name */ /**App Router 📁test-page L page.tsx //page.tsxという名前は固定 => localhost:3000/test-page */
これでルーティングが設定できるから大変楽。
・開発サーバの部分的な高速リロード(Fast Refresh)
=> 更新時に、関係のないstateは保持したまま、他の要素を更新できる。
あたりがめちゃくちゃ便利だなと感じています。
next.jsを書く上で抑えておいた方がよさそうなこと
・App Router vs Page Router
基本的に、App Routerが推奨のようです。
App Router | Page Router |
---|---|
appディレクトリ配下の、ディレクトリ名がページ名になる。 例) app/hogehoge/page.tsx => localhost:3000/hogehoge |
pagesディレクトリ配下の、ファイル名がページ名になる。 例) pages |
全てのページが、デフォルトでサーバーコンポーネントになる。 | 全てのページが、デフォルトでクライアントコンポーネントになる。 |
・サーバーコンポーネントと、クライアントコンポーネントの違い
・App Routerで作成したページは、デフォルトでサーバーコンポーネントになります。サーバーコンポーネントとは、"レンダリングがサーバーサイドで行われるコンポーネント"のことであり、クライアントコンポーネントはその逆です。
もしApp Routerでクライアントでレンダリングさせたい場合は一行目に"use client"と書けばよいようです。
そもそもなぜ二種類のコンポーネントがあるのか
これにはRSC(React Server Components)という技術が深くかかわってきます。
RSCとは、コンポーネントを、クライアントサイドでレンダリングするものと、サーバーサイドでレンダリングするものの二つに分けて、効率的にレンダリング処理を行う技術です。
例えばデータ取得のfetchなどは、よりDBに近いサーバーサイドで行った方が、リクエスト数も減ったりするメリットがあります。
そもそもAPIへのアクセスとかDBへのアクセスは、サーバーサイドで行った方がセキュリティ的にも安全でしょう。
こちらに関しては、以下の記事がわかりやすかったので是非ご覧ください。
https://qiita.com/newbee1939/items/7ce919f9a1a7153582b8
RSCとSSGを組み合わせることで、より効率化できそうですね。
・サーバーコンポーネント vs クライアントコンポーネント
引用: https://levtech.jp/media/article/column/detail_399/
サーバーコンポーネントではuseEffectやuseStateは使えないということですね。
ディレクトリ構成どうする
コロケーションしない vs コロケーションする
コロケーション重視でディレクトリ切った方が後々管理が楽そうな予感。
// コロケーションしない(引用)
├── app ... ルーティングに関するコンポーネント
| ├── blog
│ ├── [uuid]
│ │ └── page.tsx
│ │ └── edit
│ │ └── page.tsx
│ ├── create
│ │ └── page.tsx
│ └── page.tsx
├── login
│ └── page.tsx
├── profile
│ └── page.tsx
|
├── features ... ロジック + コンポーネントをまとめたもの
| ├── common ... 共通部分
| | |
| │ ├── editors
| │ │ ├── components
| │ │ │ ├── CodeEditor.tsx
| │ │ │ └── RichTextEditor.tsx
| │ │ ├── hooks.ts
| │ │ └── stores.ts
| │ ├── forms
| │ │ ├── components
| │ │ │ ├── Input.tsx
| │ │ │ └── Select.tsx
| │ │ ├── hooks.ts
| │ │ └── stores.ts
| │ └── tags
| │ ├── components
| │ │ ├── TagItem.tsx
| │ │ └── TagList.tsx
| │ ├── hooks.ts
| │ └── stores.ts
| |
| └── routes ... 特定のページで使うもの
| └── auth
| ├── components
| │ └── Login.tsx
| ├── endpoint.ts
| └── hooks.ts
|
├── components ... ロジックがない共通コンポーンネント
├── hooks ... 共通ロジックの内、React Hooksが「ある」もの
├── utils ... 共通ロジックの内、React Hooksが「ない」もの
└── constants ... 定数を定義したファイル
//コロケーションする(想像)
├── app
│ ├── layout.ts ... 共通レイアウト
│ ├── page.ts ... トップページ
│ ├── about ... Aboutページ
│ │ ├── layout.ts
│ │ ├── page.ts
│ │ ├── component.ts ... Aboutページ特有のコンポーネント
│ │ ├── hooks.ts ... Aboutページ特有のReact Hooks
│ │ └── styles.module.css ... Aboutページ特有のスタイル
│ └── blog ... Blogページ
│ ├── layout.ts
│ ├── page.ts
│ ├── post.ts ... Blogページ特有のコンポーネント
│ ├── hooks.ts ... Blogページ特有のReact Hooks
│ └── styles.module.css ... Blogページ特有のスタイル
├── components ... 共通コンポーネント
│ ├── Header.ts
│ └── Footer.ts
├── hooks ... 共通のReact Hooks
│ └── useAuth.ts
├── utils ... 共通のユーティリティ
│ ├── api.ts
│ └── helpers.ts
├── constants ... 定数を定義したファイル
│ └── config.ts
└── styles ... グローバルスタイル
└── globals.css
-
コロケーションしない
・メリット
- 同種ファイルがまとまっているので、一元管理が可能。
・デメリット
- プロジェクトが大きくなってくると、目的の機能を探すことが困難になってくる。
- ディレクトリが大きく分かれているので、あっちこっち行くことになる。
-
コロケーションする
・メリット
- 特定のページに関する機能が近くにあるので、機能を探しやすい。
- 関係性の高いファイルが近くにあるので、リファクタや変更がしやすい。
- ファイルの行き来が減る。
・デメリット
- プロジェクトが大きくなってくると、そのディレクトリがめちゃくちゃ複雑になる
書き方のルール
・特別なファイル名
next.jsには特別なファイル名が用意されています。それが以下の画像です。
引用: https://nextjs.org/docs/app/building-your-application/routing
名前 | 意味・用法 |
---|---|
layout | 同じルートのページに対して、共通で表示させられるUI。 例) 共通のヘッダーとかフッターを記載 |
page | app routerを使うときの、エントリーポイントになるところ。これがないとルーティングが認識されない。 例) app/login/page.tsx |
loading | 処理に時間がかかるコンポーネントを表示するときに、非同期で呼び出し、ローディング画面を表示することができる。Suspenceを使うと個別にコンポーネントに対して非同期呼び出しができるようになる。 例) <Suspence fallback={<Loading />}> ロード終了 </Suspence>
|
not-found | 404ページ。404エラーが発生したとき、このページが表示される。 |
error | エラーページを出したいときに使う。上位階層のlayout.tsx や template.tsxでおきたエラーは補足できない。 |
global-error | より上位階層のエラーを回収するためにあるらしい? |
route | api作成に用いる。.com/test-apiにアクセスすることで、APIとして使える。 |
template | 基本はlayoutと同じだが、こちらは遷移時にstateの更新が行われる。 例) form要素をページ遷移で初期化したいとき。 |
default | 何らかの原因でページが読み込まれなかったときに表示される内容のようです。 |
基本的に以下の階層で差し込まれるようです。
<Layout>
<Template>
<ErrprBoundry fallback={<Error />}>
<Suspense fallback={<Loading />}>
<Page />
</Suspense>
</ErrprBoundry>
</Template>
</Layout>
defaultの挙動の検証
例えば、以下のようなディレクトリを用意します。
parallelTest
├── @testA
│ └── setting
│ └── page.tsx
├── @testB
│ ├── default.tsx
│ └── page.tsx
├── default.tsx
├── layout.tsx
├── page.tsx
└── styles.module.css
各ファイルの中身(一部)
"use client"
import styles from '../../styles.module.css';
export default function testA() {
return (
<div className={styles.testA}>this is testA!!!</div>
)
}
"use client"
import styles from '../styles.module.css';
export default function testB() {
return (
<div className={styles.testB}>this is testB!!!</div>
)
}
"use client"
import styles from '../styles.module.css';
export default function Default() {
return (
<div className={styles.testB}>Default</div>
)
}
"use client"
import styles from './styles.module.css';
export default function Default() {
return (
<div className={styles.testB}>Parent Default</div>
)
}
この状態でhttp://localhost:3000/parallelTest/setting にアクセスすると、以下のようになります。
これは、@testAディレクトリにはsettingディレクトリがありますが、@testBディレクトリには存在せず、読み込むことができなかったため、代わりにdefault.tsxの内容が表示されているようです。
ところで、parallelTestに配置したdefault.tsxも表示されています(「Parent Default」のやつ)。
これはなぜでしょうか。
もう一度構造を見てみましょう。
parallelTest
├── @testA
│ └── setting
│ └── page.tsx
├── @testB
│ ├── default.tsx
│ └── page.tsx
├── default.tsx
├── layout.tsx
├── page.tsx
└── styles.module.css
考えなくてはいけないのは、settingディレクトリの有無です。
よく見るとparallelTest直下にはsettingディレクトリがありません。
@testAディレクトリはルーティングに影響を及ぼさないだけで、settingディレクトリではないので、settingディレクトリを見つけられなかったparallelTestディレクトリはdefault.tsxを返しました。
ちゃんと配置してあげれば、default.tsxの内容は表示されません。
parallelTest
├── @testA
│ ├── default.tsx
│ └── setting
│ └── page.tsx
├── @testB
│ ├── default.tsx
│ └── page.tsx
├── default.tsx
├── layout.tsx
├── page.tsx
├── setting // これ
│ └── page.tsx //これ
└── styles.module.css
・特殊なルーティングについて
・プライベートフォルダ
フォルダ名の最初にアンダースコア(_)をつけることでそれ以下のフォルダではページと認識されないようです。
├── app
│ ├── login
│ │ ├── _component //こいつはルーティングに含まれない
| | | ├── page.tsx // 表示されない
│ │ │ └── withAuth.tsx
│ │ ├── error.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── template.tsx
・ルートグループ
フォルダ名を" () "でくくることで、URLパスに影響を及ぼさず、ルートの整理が行えます。例えば、以下のように管理者側と、一般ユーザー側を明示的にグルーピングできたりします。
├── app
│ ├── homepage
│ │ ├── (admin)
│ │ │ ├── edit
│ │ │ │ └── page.tsx // localhost:3000/homepage/edit
│ │ │ └── regist
│ │ │ └── page.tsx
│ │ └── (client)
│ │ ├── blog
│ │ │ └── page.tsx // localhost:3000/homepage/blog
│ │ └── contact
│ │ └── page.tsx
・ダイナミックルーティング
フォルダ名を" [] "でくくることで、動的なルーティングが実装できます。
例えば、app/homepage/blog/[id]/page.tsx
のように書き、以下を記載します。
export default function Page({ params }: { params: { id: string } }) {
return <div>My Post: {params.id}</div>
}
//http://localhost:3000/homepage/blog/1 => My Post: 1
//http://localhost:3000/homepage/blog/2 => My Post: 2
アクセスするURLによってidの値は変わり、動的なルーティング設定ができます。
もし
http://localhost:3000/homepage/blog/study/1
みたいに、複数階層にわたってパラメータを取得したい場合は以下のように書き換えます。
app/homepage/blog/[...id]/page.tsx
export default function Page({ params }: { params: { id: string[] } }) {
return <div>My Post: {params.id.join(',')}</div>
}
//=> My Post: study,2
or
app/homepage/blog/[category]/[id]/page.tsx
export default function Page({ params }: { params: { category: string, id: string } }) {
return <div>My Post: {`${params.category}:${params.id}`}</div>
}
//=> My Post: study,2
しかし、このままだと、
http://localhost:3000/homepage/blog
のように、階層がない場合、404エラーになってしまうため、以下のように書き換えます。
app/homepage/blog/[[...id]]/page.tsx
export default function Page({ params }: { params: { id: string[] } }) {
return (
(params.id) ? <div>My Post: {params.id.join(',')}</div> : <div>no id</div>
)
}
・平行ルーティング
フォルダ名の先頭に@をつけることで、複数のコンポーネントを並列に描画することができます。
例えば、以下のような構成で、ファイルを作成します。
parallelTest
├── @testA
│ └── page.tsx
├── @testB
│ ├─ loading.tsx
| └── page.tsx
|
├── layout.tsx
├── page.tsx
└── styles.module.css
"use client"
export default function Loading() {
return <div>loading...</div>
};
"use client"
import styles from '../styles.module.css';
export default function testA() {
return (
<div className={styles.testA}>this is testA!!!</div>
)
}
"use client"
import { Suspense } from 'react';
import styles from '../styles.module.css';
import Loading from './loading';
export default async function testB() {
const Await = () => {
return new Promise(callback => { setTimeout(callback, 3000) })
};
await Await();
return (
<Suspense fallback={<Loading />}>
<div className={styles.testB}>this is testB!!!</div>
</Suspense>
)
}
"use client"
import styles from './styles.module.css';
export default function Layout({
children,
testA,
testB,
}: {
children: React.ReactNode
testB: React.ReactNode
testA: React.ReactNode
}) {
return (
<>
{children}
<div className={styles.testContainer}>
{testA}
{testB}
</div>
</>
)
}
非同期処理でも、同時並列的に表示できていることがわかると思います。
・インターセプトルート
フォルダ名の先頭に(.)をつけることで、対象のパスを乗っ取ることができるようです。
このサイトが非常に参考になりました。
先頭のパスは、
(.) => カレントディレクトリ
(..) => 親ディレクトリ
(..)(..) => 二つ上のディレクトリ
(...) => appディレクトリからの相対パス
になるそうです。
簡単に説明すると、インターセプトルートを使うことで、指定したパスの表示を奪うことができます。
公式サンプル: https://github.com/vercel/nextgram
このサンプルは以下の構造になっています。
app
├── @modal
│ ├── (..)photos
│ │ └── [id]
│ │ └── page.js
│ └── default.js
├── default.js
├── global.css
├── layout.js
├── opengraph-image.png
├── page.js
└── photos
└── [id]
└── page.js
@modal/(..)photos で、app/photosのパスを乗っ取っているので、平行ルーティングも併せて、modalと普通の写真ページが同時に表示されています。
うまく使うの難しそう...
まとめ
・next.jsの強みは、ページごとにレンダリング方法を指定できるので、より効率的でSEO対策に強いサイトを作ることができること。加えてルーティングの設定が容易であること。
この原則を崩さないように思想を叩き込んでコーディングしていきたいなと思います。
長くなっちゃうので具体的なSSGとかSSRの書き方とかは別記事で...
ここまで読んでくださってありがとうございました!