はじめに
こんにちは! yu-Matsu と申します。
現在Reactでのフロントエンド開発を絶賛勉強中なのですが、そろそろNext.jsが気になる今日この頃だったので、少し触れてみました。今回はその体験記を記事にしたいと思います。なお、今回の記事では Server Components には深く触れません。 こちらは次回記事にしたいと思っています。
そもそもApp ディレクトリって?
今回はあくまで体験記ですので詳しい解説は行いませんが、2023年5月に公開されたバージョン13.4でApp Routerがstableになりました。これは、従来のPage Routerに代わる機能になります。
Page Routerでは、pagesというディレクトリの配下に作成したいページ名のファイル(XXX.tsx)を作成するだけで自動でルーティングが設定されるという便利なものでしたが、App Routerでは、appというディレクトリの配下に作成したいページのディレクトリを作成し、さらにその配下にpage.tsxを作成することで、Page Routerと同様にルーティングが設定されます。今回の記事ではこのappディレクトリに着目したいと思います。
app/
|- example1/
| page.tsx
|
|- example2/
| page.tsx
|...
実際に試してみる
では実際に、以下のような仕様のWebアプリケーションを作成しながらAppディレクトリを体験したいと思います。
- ログインページでユーザー名とパスワードを入力するとログイン出来る
- ログイン後、TOPページに遷移する。TOPページではChatGPTと会話できるChatページへの遷移ボタンが配置されている
- ChatページではChatGPTとテキストベースで会話が出来る(下画像のようなイメージ)
ChatページのバックエンドはAWS上で構築していまして、本筋から外れるため詳細は省きますが、APIGateway + Lambda + DynamoDBのサーバーレス構成です。ちなみにずんだもんっぽく喋るようにしています。語尾が不安定ですが...(こちらも機会があれば記事にしたい...)
プロジェクトの作成
まずはプロジェクトを作成してきます。こちらの手順は様々なNext.jsの記事で紹介されていますが、一応念のため本記事でも掲載します。create-next-app コマンドを利用してプロジェクトの作成を実施します。プロジェクト名は任意です。
% npx create-next-app@latest
✔ What is your project named? … nextjs-project
✔ Would you like to use TypeScript? … No / Yes → Yes
✔ Would you like to use ESLint? … No / Yes → Yes
✔ Would you like to use Tailwind CSS? … No / Yes → No
✔ Would you like to use `src/` directory? … No / Yes → Yes
✔ Would you like to use App Router? (recommended) … No / Yes → Yes
✔ Would you like to customize the default import alias? … No / Yes → Yes
✔ What import alias would you like configured? … @/*
Creating a new Next.js app in [作成先]
Using npm.
Initializing project with template: app
Installing dependencies:
- react
- react-dom
- next
- typescript
- @types/react
- @types/node
- @types/react-dom
- eslint
- eslint-config-next
added 297 packages, and audited 298 packages in 21s
120 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Initialized a git repository.
Success! Created nextjs-project at [作成先]
プロジェクトの作成に成功すると、上記コマンドを実行した場所にプロジェクト名のディレクトリが作成されます。ディレクトリの構成は以下のようになっています。
nextjs-project/
|- app/
| global.css
| layout.tsx
| page.tsx
| page.module.css
|
|- node_modules/
| ...
|
|- public/
| next.svg
| ...
|
|- next-dev.d.ts
|- next.config.js
|- package.json
|...
それでは、npm run dev コマンドを実行してみましょう
% npm run dev
> nextjs-project@0.1.0 dev
> next dev
- ready started server on 0.0.0.0:3000, url: http://localhost:3000
- info Downloading WASM swc package...
...
Webブラウザで localhost:3000 にアクセスすると以下のようなページが表示されます。これで準備が整いました!初めてReactを触った時もそうでしたが、プロジェクトを新規作成してローカル起動して表示されるページを見ると、なぜかワクワクします。
appディレクトリ配下を触ってみる
appディレクトリを見てみると、page.tsxの他に layout.tsx というファイルがあるかと思います。このファイルで、各ページで共通するコンポーネントやスタイルをレイアウトとして定義することが出来ます。各ページのディレクトリ配下に配置可能ですが、appディレクトリ直下では必ず配置する必要があります。Page Routerで言うところの_document.jsや_app.jsに当たるものになります。layout.tsxは以下のようになっています。
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
page.tsxファイルをラップする形で html タグや body タグが設定されていますね。全体のスタイルを定義しているglobals.cssもここでimportしています。また、metadataの定義もここでしています。App Routerでは、メタ情報をmetadataオブジェクトをexportすることで設定しているようです。実際にmetadataオブジェクトのtitleを「Nextjs Project」に変更すると...
反映されました!
TOPページも編集していきます。
const Home: React.FC = () => {
return <h1>TOPページ</h1>;
};
export default Home;
こんな感じになりました。
スタイルにサンプルページの名残があるので、global.cssも編集していきます。とりあえず、背景色の設定だけ行います。
html,
body {
background-color: #f0ffff;
}
ヘッダーを追加してみる
先ほど、layout.tsxでは各ページで共通するコンポーネントをレイアウト出来るとお伝えしましたので、ヘッダーを追加してみたいと思います。まずはHeaderコンポーネントを作成します。今回は、コンポーネントはプロジェクト直下にcomponentsディレクトリを作成し、そこにまとめていくことにします。
nextjs-project/
|- app/
|...
|- components/
| |- Header/
| | index.tsx
| | header.module.css
|...
Headerコンポーネントな以下のようなものを作成しました。Webアプリケーションの名前を表示するだけの単純なもので、名前部分をクリックするとTOPページに戻るようにしています。
import styles from "./header.module.scss";
import { useRouter } from "next/navigation";
export const Header: React.FC = () => {
const router = useRouter();
return (
<div className="headerArea">
<h1 className="headerText" onClick={() => router.push("/")}>
Next.jsの勉強
</h1>
</div>
);
};
では、layout.tsxに追加してみましょう。
+ import { Header } from "@/components/Header";
...
<body className={inter.className}>
+ <Header />
{children}
</body>
</html>
);
}
これでヘッダーが表示される!と思いきや、以下のようなエラーが発生しました...
エラー内容を見てみると、「useRouter only works is Client Components」 とあります。調べてみると、Next.js v13以降のコンポーネントは デフォルトでServer Componentsとして動作する ため、 routerやReact hook、localstorageなどのブラウザ側の機能を利用できないとのことです。これらの機能を使いたい場合は、"use client" の設定が必要のようです。
では、Headerコンポーネントに"use client"の設定を追加してみます。
+ "use client";
import styles from "./header.module.scss";
...
無事にヘッダーが表示されました!これはつまづきポイントですね... Server ComponentsとClient Componentsの違いを理解していないとしょっちゅうこのエラーに遭遇しそう...
ログインページを作ってみる
次に、ログインページを実装してみたいと思います。ユーザーの管理は AWS Cognito で行なっていまして、Cognitoのユーザープールに既にユーザーが作成されているものとします。
今回は、AWS Amplifyを使わない(正確にはライブラリは使う)実装にしました。以下の記事の内容をほぼそのまま使わせていただきました。先人たちに感謝です...!
上記記事に記載されている通りに、Cognitoの設定ファイル(auth.ts)、authフック(use-auth.tsx)、ログインステータスによってページを出し分けるためのラッパー(PrivateRoute/index.tsx)を作成していきます。それぞれ以下の場所に作成しました。
nextjs-project/
|- app/
|...
|- components/
| |...
| |- PrivateRoute/
| | index.tsx
| |...
|
|- hooks/
| use-auth.tsx
| auth.ts
|...
基本的には上記記事のコードを使わせていただいていますが、PrivateRouteのみNext.js仕様に書き換えています。
import { redirect } from "next/navigation";
import { useAuth } from "@/hooks/auth_hook";
type Props = {
children?: React.ReactNode;
};
const PrivateRoute: React.FC<Props> = ({ children }) => {
const auth = useAuth();
if (!auth.isAuthenticated) {
redirect("/login");
}
return <>{children}</>;
};
export default PrivateRoute;
では、ログインページを実装していきます。と言っても、こちらも上記記事のコードをほぼそのまま使わせていただくことになりますが、Appディレクトリの機能によるルーティングを実現するために、appディレクトリ配下に login/page.tsx を作成していきます。
nextjs-project/
|- app/
| |...
| |- login/
| | page.tsx
| | login.module.css
|...
こちらもNext.js仕様に少し修正しています。スタイルもこの時点で当てていますが、省略します。
+ "use client";
...
- import { useNavigate } from 'react-router-dom';
+ import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/auth_hook";
const Login = () => {
...
const executeSignIn = async (event: React.FormEvent<HTMLFormElement>) => {
...
- navigate({ pathname: '/' });
+ router.push("/");
...
};
...
後は、ログインの状態、情報を他のページでも利用出来るように、layout.tsxを修正、TOPページでログインステータスを確認し、ログインがされていなければログインページにリダイレクトさせるために、TOPページを修正します。ついでにログインしているユーザー名を表示してみます。
+ import { ProvideAuth } from "@/hooks/auth_hook";
...
export default function RootLayout({
...
<body className={inter.className}>
+ <ProvideAuth>
<Header />
{children}
+ </ProvideAuth>
</body>
...
+ import PrivateRoute from "@/components/PrivateRoute";
+ import { useAuth } from "@/hooks/auth_hook";
const Home: React.FC = () => {
+ const { username, isLoading } = useAuth();
+ if (isLoading) return <h1>Loading...</h1>;
return (
+ <PrivateRoute>
<h1>TOPページ</h1>
+ <p>ユーザー名: {username}</p>
+ </PrivateRoute>
);
};
export default Home;
これでログイン画面が表示されるかな...、と思いきや、またエラーが出ました...
なるほど、確かにTOPページはServer Components、PrivateRouteで呼び出しているuseAuthはClient側の関数ですので、怒られているようです。ここもつまづきポイントですね...。 Server ComponentsとClient Componentsが混在する場合は気をつけないとな...
解決法として、TOPページで"use client"を設定しても良いのですが、Next.jsではパフォーマンス向上のため、Client Componentはできる限りコンポーネントツリーの端(葉)の部分に配置するように推奨されている ようですので、TOPページのコンテンツをClient Componentsとして外出ししたいと思います。(ついでにスタイルも当てました)
...
|- components/
| |...
| |- TopContents
| | top.module.css
| | index.tsx
| |
...
"use client";
import styles from "./top.module.css";
import PrivateRoute from "../PrivateRoute";
import { useAuth } from "@/hooks/auth_hook";
export const TopContents: React.FC = () => {
const { username } = useAuth();
return (
<PrivateRoute>
<div className="container">
<h1 className="pageTitle">TOPページ</h1>
<div className="box">
<div>
<p className="userName">ユーザー名:{username}</p>
</div>
</div>
</div>
</PrivateRoute>
);
};
import { TopContents } from "@/components/TopContents";
const Home: React.FC = () => {
return <TopContents />;
};
export default Home;
エラーが解消され、無事にログインページが表示されました!!
Cognitoユーザープールに作成済みのユーザーでログインしてみると...
ログインに成功し、ログイン時のユーザー名が取得できています!
この時点で、appディレクトリ配下に login というディレクトリを作成するだけで、ルーティングが出来てしまうことが確認出来ました!
Chatページの作成
最後にChatページを作成していきたいと思います。まずChatページの枠だけ作成し、TOPページから遷移できることを確認します。
app/
|...
|- chat/
| page.tsx
| chat.module.css
...
import styles from "./chat.module.scss";
const Chat: React.FC = () => {
return (
<div>
<h1>Chatページ</h1>
</div>
);
};
export default Chat;
TOPページからはChatページに遷移するためのボタンを設置します。
+ import { useRouter } from "next/navigation";
export const TopContents: React.FC = () => {
...
+ const router = useRouter();
export const TopContents: React.FC = () => {
const { username } = useAuth();
const router = useRouter();
return (
...
<p className={styles.userName}>ユーザー名:{username}</p>
+ <div>
+ <button
+ className={styles.button}
+ onClick={() => {
+ router.push("/chat");
+ }}
+ >
+ Chatページへ
+ </button>
+ </div>
</div>
...
実際にTOPページにボタンが配置され、押下するとChatページに遷移することが確認出来ます。
後は、Chatページのコンポーネントを作成していきます。Chatページでは、ユーザーがメッセージを入力し、「投稿」ボタンを押下するとAPIが呼ばれる想定ですので、APIを呼ぶ部分を外出ししておきたいと思います。プロジェクト直下にapiというディレクトリを作成して、そこに作成していきます。appディレクトリ配下に作成しない理由としては、本記事では紹介しない API Routes という機能を利用する際、appディレクトリ配下にapiディレクトリを作成することになるためです。
nextjs-project/
|- api/
| chatgpt.ts
|- app/
...
import axios from "axios";
export interface PostData {
message: string | null;
sessionId: string | null;
}
/**
* 自作のchatGPTのAPIを呼び出す
* @param {postData} data APIに渡す情報
*/
export const postChat = async (data: PostData) => {
const response = await axios.post(
"https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/api/chat",
data
);
return response;
};
簡単にパラメータの説明をすると、message はユーザーが入力したメッセージ、sessionId は、ChatGPTとの会話履歴(のようなもの)をDynamoDBに保持しており、そのキーになります。sessionIdはログイン時のユーザー名に紐づいています。
Chatコンポーネントは以下のようになります。
...
|- components/
| |...
| |- ChatComponent
| | chat_component.module.css
| | index.tsx
| |
...
"use client";
import styles from "./chat_component.module.scss";
import PrivateRoute from "../PrivateRoute";
import React, { useState } from "react";
import { postChat } from "@/api/chatgpt";
import { useAuth } from "@/hooks/auth_hook";
interface ChatData {
actor: string;
content: string;
}
export const ChatComponent: React.FC = () => {
const { username, isLoading } = useAuth();
/** ユーザーが入力したメッセージのstate */
const [content, setContent] = useState<string>("");
/** 現在のセッションでの会話履歴のstate */
const [chatList, setChatList] = useState<ChatData[]>([]);
if (isLoading) return <h1>Loading...</h1>;
const onChangeContent = (event: React.ChangeEvent<HTMLInputElement>) =>
setContent(event.target.value);
/**
* ユーザー名とユーザーが入力したメッセージをパラメータとして、APIを呼び出す
* 会話履歴を表示するために、ユーザーのメッセージとAPIのレスポンスをchatListに追加する
*/
const postMessage = async () => {
const data = {
message: content,
sessionId: username,
};
/** ユーザーの入力した内容をchatListに追加 */
setChatList((prevChatList) => [
...prevChatList,
{ actor: "human", content: data.message },
]);
try {
const res = await postChat(data);
/** chatGPTのメッセージをchatListに追加 */
setChatList((prevChatList) => [
...prevChatList,
{ actor: "ai", content: res.data },
]);
} catch (error) {
console.log(error);
}
setContent("");
};
/** ユーザーのメッセージとChatGPTのメッセージでスタイルを変えている
* (某メッセージアプリをイメージ)
*/
return (
<PrivateRoute>
<div className="chatArea">
{chatList.map((item, index) => {
return (
<div className="chatContainer" key={index}>
<p
key={index}
className={
item.actor === "ai" ? "chatAi" : "chatHuman"
}
>
{item.content}
</p>
</div>
);
})}
</div>
<div className="postChat">
<input
id="postChatContent"
type="text"
placeholder="投稿内容"
value={content}
onChange={onChangeContent}
/>
<button id="postBtn" onClick={postChat}>
投稿
</button>
</div>
</PrivateRoute>
);
};
import styles from "./chat.module.scss";
+ import { ChatComponent } from "@/components/ChatComponent";
const Chat: React.FC = () => {
return (
<div className="container">
<h1 className="pageTitle">Chatページ</h1>
+ <ChatComponent />
</div>
);
};
export default Chat;
Chatページの動作確認として、適当に「こんにちは」とメッセージを送ってみたところ...
回答が返ってきました!実装済みのAPIも問題なく呼び出せています。
これで、最初の方に提示した仕様要件を満たしたWebアプリケーションの作成が完了しました!
まとめ
今回はNext.js入門の入り口ということで、Appディレクトリに慣れるために簡単なWebアプリケーションを作成してみました。
感想としては、とにかくルーティングがページに対応したディレクトリを作成するだけで実現出来るのはかなり便利だと思いました。従来のReactだと、BrowserRouterなどを利用して、自分でルーティングを定義する必要がありましたが、その手間が省略されただけでもとっつき易くなったのでは、と思います。その一方で、Client ComponentsとServer Compontensをしっかりと理解し、意識して使い分けていかないと、思わぬエラーに遭遇したり、思うように実装出来なかったりすると感じました。
次回は本格的に Server Compontents や App Router などに触れてみた感想を記事にしたいと思いますので、お楽しみに!それでは、本記事を読んでいただきましてありがとうございました!