初めてでもわかるNext.jsの基礎(React)

これまで一度もNext.jsを触ったことがないけど興味があるまた今後使ってみたいという人を対象にNext.jsの基礎について説明を行っています。
本文書を読み終えると下記のことを理解することができます
- Next.jsのインストール方法
- 静的ファイルの作成方法
- 動的ファイルの作成方法
- ページ間のリンクの設定方法
- 外部からのデータ取得と表示(getStaticProps, getServerSideProps)
- styled-jsxによるCSSの適用方法
Next.jsをインストールするためには事前に Node.jsのインストールを完了しておく必要があります。
環境:macOS Catalina バージョン10.15.6、Next.jsはバージョンは9.5.3です。
Next.jsのインストール
npxコマンドを利用してnext.jsのインストールを行います。
% npx create-next-app
プロジェクトの名前を聞かれるので任意のプロジェクト名を入力してください。デフォルトの名前のまま進む場合はEnterを押します。コマンドを実行したディレクトリに任意の名前もしくはmy-appディレクトリが作成されます。
インストール中にエラー発生
※下記のエラーは環境に依存するエラーがだと思いますので発生しない場合はスキップしてください。
インストール処理は最後までいきその後にnpm run devコマンドを実行してもNext.jsの初期画面は表示されるのですが、インストールログに下記のエラーが表示されていることを確認。
gyp: No Xcode or CLT version detected!
gyp ERR! configure error
gyp ERR! stack Error: `gyp` failed with exit code: 1
gyp ERR! stack at ChildProcess.onCpExit (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:351:16)
gyp ERR! stack at ChildProcess.emit (events.js:314:20)
gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:276:12)
gyp ERR! System Darwin 19.6.0
gyp ERR! command "/usr/local/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
gyp ERR! cwd /Users/mac/Desktop/my-app/node_modules/fsevents
gyp ERR! node -v v14.7.0
gyp ERR! node-gyp -v v5.1.0
gyp ERR! not ok
command line toolsに問題が発生しているということなのでパスを確認して削除を行います。
% xcode-select --print-path
/Library/Developer/CommandLineTools
% sudo rm -rf /Library/Developer/CommandLineTools
削除後commend line toolsのインストールを行うため下記のコマンドを実行しますが、”このソフトウェアは、現在ソフトウェア・アップデート・サーバから入手できないため、インストールできません。”と表示されインストールすることができませんでした。
% xcode-select --install
Appleのサイト(https://developer.apple.com/download/)から手動でインストールすることができるということなので、アクセスを行います。アクセスにはApple IDが必要となります。
下記のCommand Line Tools for Xcode 12をダウンロードしてインストールを行います。

一度npx create-next-appコマンドで作成されるmy-appディレクトリを削除再度npx create-next-appコマンドを実行すると今回はエラーなしでインストールが完了しました。
next.js開発サーバの起動
インストール中に表示されるメッセージの中にnpmの3つのコマンドの説明があります。
Success! Created my-app at /Users/mac/Desktop/my-app
Inside that directory, you can run several commands:
npm run dev
Starts the development server. //開発時に実行。開発サーバを起動
npm run build
Builds the app for production. //本番用にビルドを行う
npm start
Runs the built app in production mode.// 本番用にビルドしたアプリを起動
We suggest that you begin by typing:
cd my-app
npm run dev
本書では開発を行うので最初に表示されているnpm run devコマンドを実行します。
% cd my-app
% npm run dev
> my-app@0.1.0 dev /Users/mac/Desktop/my-app
> next dev
ready - started server on http://localhost:3000
event - compiled successfully
event - build page: /next/dist/pages/_error
wait - compiling...
event - compiled successfully
event - build page: /
wait - compiling...
event - compiled successfully
localhost:3000にアクセスすると下記の画面が表示されます。これでNext.jsのインストールは完了です。

Hello Next.js
ディレクトリ構成
ディレクトリ構成の確認を行います。エディターはVisual Studio Codeを利用しています。ディレクトリはpages, styles, node_modules, publicの4つです。その他にREADME.mdとpackage.jsonとpackage-lock.jsonの3つのファイルがそ存在します。
ブラウザで表示されたページの内容はpagesディレクトリの中のindex.jsファイルに記述されています。このフォルダの中にアプリケーションのコアとなるコードを記述していくことになります。

publicディレクトリの中には、favicon.ico, vercel.svgファイルが保存されています。これらのファイルはブラウザから直接アクセスすることができます。
URLのlocalhost:3000の後ろに/vercel.svgを追加してください。publicディレクトリに保存したファイルには直接ブラウザからアクセスできることが確認できました。publicには静的なファイルを保存できることがわかります。例えばabout.htmlファイルをpublicフォルダに作成してもブラウザから直接アクセスでき、その内容を表示させることが可能です。

stylesディレクトリの下にはCSSのファイルを保存します。しかし、CSSファイルについてはpublicディレクトリに作成してJavaScriptのバンドルとしてではなく直接アクセスすることも可能です。
index.jsファイルの更新
index.jsファイルを更新するとブラウザ上に表示される内容も変更されるのか確認を行います。
index.jsの中身を一度削除して下記のように更新します。
export default function Home() {
return <h1>Hello Next.js</h1>;
}

npm dev runを実行していれば、index.jsを更新するとブラウザには自動で更新内容が反映されます。

ページのソースを確認してみましょう。ブラウザで右クリックして、ページのソースを表示してください。(Chromeの場合)

ソースの中にHello Next.jsファイルが入っていることがわかります。index.jsはJavaScriptファイルなのでJavaScriptファイルをブラウザが受け取って表示するのではなくブラウザが直接HTMLの情報を受け取っていることがわかります。これはNext.jsがブラウザに送信する前にpre-Renderingを行っていからです。HTMLの情報をそのまま受け取っているのでブラウザ側でのJavaScriptの処理を行う必要がありません。
これだけの情報ではpre-Renderingが行われているかはわかりません。Reactと比較してみましょう。
同じようにReactもh1タグでHello Reactと記述します。下の画像は字が細かすぎて見えないかもしれませんが、どこにもh1タグは表示されていません。つまりブラウザは受信したJavaScriptファイルを利用してh1タグを描写していることがわかります。

ブラウザのソースを見るとことでClient RenderingとServer-side Renderingの違いも理解することができました。
別のページを作成する(静的ファイル)
Next.jsではpagesディレクトリにファイルを作成するだけで自動でルーティングが設定されるため簡単に静的ページを作成することができます。
pagesディレクトリの下にabout.jsファイルを作成します。index.jsと同様に下記のコードを記述します。h1タグの中身と関数名のみ変更しています。
export default function About() {
return <h1>About Page</h1>;
}
URL:localhost:3000/aboutページにアクセスするとAbout Pageが表示されます。Next.jsを利用するとルーティングの設定を行うことなく簡単にページが作成できることが確認できます。

ここまでの設定では”/”(ルート)と/aboutページ以外にアクセスを行うと404エラーが表示されます。


pagesディレクトリ内にディレクトリを作成しその下にjsファイルを作成した場合の動作も確認しておきます。
まずpagesの下にproductsディレクトリを作成し、bag.jsファイルを作成します。
export default function Bag() {
return <h1>バックのページです</h1>;
}
想像はつくとは思いますが、localhost:3000/products/bagにアクセスするとbag.jsファイルの中身が表示されます。ページの階層化も行うことができます。

動的ファイルの作成(ダイナミックルーティング)
productsの下にはbag以外にもさまざまな商品があるとします。ダイナミックルーティングを利用してURLのproduct/の次にどんな文字列を入れてもブラウザにファイルの内容が表示されるように設定を行います。
productsディレクトリの下にスクエアブラケットで囲まれた[name].jsファイルを作成します。
export default function Name() {
return <h1>商品のページです</h1>;
}
/products/bagでもアクセスすることが可能ですが、bagをclothesやshoesに変更しても同じ内容が表示されます。

しかしURLをbagやshoesに変更しても同じ”商品のページです”が表示されます。URLに入れた文字列をページ内容に表示させることでページの内容を動的に変更させるためuseRouterを利用します。
useRouterはnext/routerからimportします。
import { useRouter } from "next/router";
export default function Name() {
const router = useRouter();
return <h1>商品{router.query.name}のページです</h1>;
}
URLに含まれる文字列はrouter.query.nameから取得することができます。
ブラウザで確認するとURLに含まれる文字列を表示することができました。

console.logを利用してrouter.queryに含まれる情報を確認します。
import { useRouter } from "next/router";
export default function Name() {
const router = useRouter();
console.log(router.query);
return <h1>商品{router.query.name}のページです</h1>;
}
デベロッパーツールで確認すると下記のようにオブジェクトの中にnameが含まれていることがわかります。
{name: "shoes"}
name: "shoes"
__proto__: Object
またURLにパラメータをつけてもrouter.queryから追加したパラメータの値を取得することが可能です。
URLにはhttp://localhost:3000/products/shoes?color=redのようにcolorのパラメータと値を設定しています。

リンクの設定
index.js, about.jsとproducts の下に[name].jsファイルを作成しましたが、リンクを設定していないのでブラウザのURLを手動で書き換える以外にページを遷移する方法がありません。ページ遷移ができるようにLinkの設定を行います。
index.jsが表示されるルートページからaboutページへの遷移を行うためLinkを利用します。Linkはnext/linkからインポートします。Linkタグの中で遷移したいページをhrefで設定します。
import Link from "next/link";
export default function Home() {
return (
<div>
<ul>
<li>
<Link href="/about">
<a>About</a>
</Link>
</li>
</ul>
<h1>Hello Next.js</h1>
</div>
);
}

ブラウザで確認するとトップページからaboutページに遷移する際にページのリロードは行われずスムーズに画面が表示されます。

もしLinkタグではなくこれまどのHTMLのようにaタグを利用して設定を行ってみてください。ページの遷移を行うことができますがページを遷移する際にページのリロードが行われページが表示されるまでに時間がかかります。
遷移先の情報の入った配列を利用してダイナミックルーティングを行いたい場合も下記のように設定をすることでページのリロードなしにページ遷移することができます。
import Link from "next/link";
const products = [{ name: "bag" }, { name: "shoes" }, { name: "socks" }];
export default function Home() {
return (
<div>
<ul>
{products.map((product) => {
return (
<li>
<Link as={`/products/${product.name}`} href="/products/[name]">
<a>{product.name}</a>
</Link>
</li>
);
})}
<li>
<Link href="/about">
<a>About</a>
</Link>
</li>
</ul>
<h1>Hello Next.js</h1>
</div>
);
}
リストの項目をクリックするとページが遷移します。

外部からデータを取得
外部から取得したデータを利用してブラウザに表示します。データの取得には、JSONPlaceholderを利用させてもらいます。
JSONPlaceholderを利用するとhttps://jsonplaceholder.typicode.com/postsにアクセスするだけで100件のPOSTS(投稿)データを取得することができます。またURLのpostsの後ろにID番号を入れることで個別のPOST(投稿)データにアクセスすることができます(posts/1, posts/2,…)。
Next.jsではサーバ側でデータを取得する方法がいくつか存在し、ここでは一般的に利用される2つの方法を確認します。
- getStaticProps(Static Generation)
- getServerSideProps(Server-side Rendering)
getStaticPropsはStatic Generationでビルド時に一度だけデータを取得して事前にページをpre-Renderingします。getServerSidePropsはServer-side Renderingでクライアントからのアクセス時にサーバ側でデータを取得しpre-Renderingします。
ブログの記事のようにクライアント側での更新がないものページにはgetStaticPropsを利用します。更新が行われるページにはgetServerSidePropsを利用します。getStaticPropsはビルド時にページが準備されているのですぐにページが表示されます。サイト、アプリケーションの高速化を実現するためにこの2つをうまく活用していく必要があります。
getServerSidePropsを利用した方法
getServerSidePropsを利用してデータを取得しページに表示させるために新たにpostsディレクトリを作成してindex.jsファイルを作成します。
作成したindex.jsファイルではgetServerSidePropsの中でJSONPlaceholderからfetchを利用してデータの取得を行っています。まずは正しくデータの取得ができているか確認するためにconsole.logを利用します。
export default function index({ posts }) {
return (
<div>
<h1>POST一覧</h1>
</div>
);
}
export async function getServerSideProps() {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const posts = await res.json();
console.log(posts);
return { props: { posts } };
}
通常であればコード中にconsole.logを記述するとブラウザのデベロッパーツールのコンソールログに情報が出力されます。しかし、getServerSidePropsはサーバ側(Next.js)で実行されるため、npm run devを実行したターミナルにpostsの100件分のデータが表示されることが確認できます。

データが取得できることが確認できたのであとはindex関数に渡したpropsをmap関数で展開します。
export default function index({ posts }) {
return (
<div>
<h1>POST一覧</h1>
<ul>
{posts.map((post) => {
return <li key={post.id}>{post.title}</li>;
})}
</ul>
</div>
);
}
export async function getServerSideProps() {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const posts = await res.json();
console.log(posts);
return { props: { posts } };
}
ブラウザで確認するとgetServerSidePropsを利用して取得したデータがブラウザ上に表示されます。

個別ページの作成と表示
次は、取得後に展開したデータとダイナミックルーティングを使って個別のPOSTデータをどのように取得し表示させるのかを確認していきましょう。
ダイナミックルーティングに対応できるようにpostsの下に[post].jsファイルを作成します。
export default function post() {
return <h1>POST(投稿)</h1>;
}
LinkをimportしてタイトルをクリックするとPOSTページに遷移するようにコードの更新を行います。
import Link from "next/link";
export default function index({ posts }) {
return (
<div>
<h1>POST一覧</h1>
<ul>
{posts.map((post) => {
return (
<li key={post.id}>
<Link href={`/posts/${post.id}`}>
<a>{post.title}</a>
</Link>
</li>
);
})}
</ul>
</div>
);
}
POST一覧画面に表示されているタイトルをクリックするとPOSTのページにページの再読み込みを行うことなくページ遷移するか確認を行ってください。ページ遷移後は以下の画面が表示されます。

POSTのidの取得
ページを遷移した時に個別ページの内容を表示させるためには、postのIDを取得して再度JSONPlaceholderにアクセスを行い、個別ページの情報を取得する必要があります。
postのIDはgetServerSidePropsのparamsを通して取得することができます。postのIDが取得できるのかどうかconsole.logを利用して確認します。
export async function getServerSideProps({ params }) {
console.log(params);
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const posts = await res.json();
return { props: { posts } };
}
POSTの一覧から個別ページにアクセスするとnpm run devを実行したターミナルにオブジェクトとして表示されます。
{ post: '1' }
ダイナミックルーティングを利用しているためでIDの値をparamsで取得しましたが、params以外にもreq, res, queryといった値も取得することができます。それらの情報を確認してみたい場合は、getServerSidePropsの引数にcontextを入れてconsole.log(context)で確認してください。reqはrequestの略でアクセスしたきたクライアント情報やヘッダー情報も確認することができます。
個別ページのデータ取得
paramsに保存されたIDを利用して個別データの取得を行います。
export default function post({ post }) {
return (
<div>
<h1>POST(投稿){post.id}</h1>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
}
export async function getServerSideProps({ params }) {
const id = params.post;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return { props: { post } };
}
ブラウザで確認すると下記のように個別のPOSTデータの内容を表示させることができます。

getStaticPropsを利用した方法
getServerSidePropsはリクエスト毎にサーバ側でデータの取得が行われますががgetStaticPropsはビルド時にデータの取得を行います。
getStaticPropsへの変更
POSTSの一覧を取得するためにpostsディレクトリのindex.jsファイルのgetServerSidePropsをgetStaticPropsに変更を行います。
export async function getStaticProps() {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const posts = await res.json();
return { props: { posts } };
}
メソッドを変更しただけではブラウザ上では何も変化がなくPOST一覧が取得できます。

次に[post].jsファイルでもgetServerSidePropsからgetStaticPropsに変更を行います。
export async function getStaticProps({ params }) {
const id = params.post;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return { props: { post } };
}
index.jsの場合はgetServerSidePropsからgetStaticPropsに変更するだけで同じようにデータが表示されましたが、[post].jsファイルではエラーが表示されます。
エラーの内容はgetStaticPathsがダイナミックSSG(ServerSide Static Generatoin)ページでは必要と記述されています。

getStaticPathsによるパス情報の取得
getStaticPathsメソッドの追加を行います。getStaticPathsメソッドの中ではpostsの一覧を取得しその中からpostsの個別ページのパス(/posts/${post.id})を取り出しています。getStaticPathsのメソッドの中ではビルド時に作成するページのパス一覧を作成して、pathsでreturnする必要があります。getStaticPathsは作成するページのパス情報を渡す役割をもっているため、getStaticPathsがない場合はパス情報がないため、先ほどのようなエラーが発生します。
export async function getStaticPaths() {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const posts = await res.json();
const paths = posts.map((post) => `/posts/${post.id}`);
return {
paths,
fallback: false,
};
}
getStaticPathsで個別ページのパス情報が取得できると個別ページにアクセスしてもエラーなしでページが表示されます。
fallbackの設定
fallbackはtrueとfalseを設定することができ、falseに設定した場合は存在するPostのページ以外にアクセスした場合に404エラーが表示されます。下記は存在しない101にアクセスを行っています。

fallbackをtureに設定した場合はサーバエラーの画面が表示されます。ビルド時に存在しなかったページが追加された場合にtureを設定しておくことでそのページにアクセスするとページのビルドが行われ作成されたページをユーザに返すことができます。

styled-jsxによるCSSの適用
styled-JSXを利用してCSSの適用を行っていきます。CSSを適用する方法は複数ありstyled-JSXを利用する必要はありません。
好き嫌いの好みのが別れそうですがJavaScriptの関数の中にCSSを記述することができます。見慣れるまでには不自然に感じてしまうかもしれませんが、特別難しいことはありません。早速使用方法を確認してみます。
h1タグの文字色、背景色を変更したい場合は下記のように記述することができます。
export default function Home() {
return (
<div>
<h1>Hello Next.js</h1>
<style jsx>
{`
h1 {
color: red;
background: green;
}
`}
</style>
</div>
);
}
styleタグの中に{“}を入れるのを忘れてないでください。それ以外は通常のCSSファイルと同様にCSSを記述することができます。

classNameを利用した適用方法
classを使って適用する場合はclassNameを利用する必要があります。classNameを利用するのはstyled-jsxを利用するときだけに限定されるものではなくJSX内でclassを利用するために使われます。
export default function Home() {
return (
<div>
<h1 className="heading">Hello Next.js</h1>
<style>
{`
.heading {
color: red;
background: green;
}
`}
</style>
</div>
);
}
Component内での適用について
index.jsファイル内に関数を使ってContentコンポーネントを作成します。Contentコンポーネント内にstyled-jsxを利用してCSSを適用します。
function Content() {
return (
<div>
<p>ここにコンテンツが入ります。</p>
<style jsx>{`
p {
color: blue;
}
`}</style>
</div>
);
}
ContentコンポーネントではpタグにCSSを適用しているので親側のHomeコンポーネントにもpタグを追加し、適用範囲を確認します。
export default function Home() {
return (
<div>
<h1 className="heading">Hello Next.js</h1>
<Content />
<p>ここにもコンテンツが入ります。</p>
<style>
{`
.heading {
color: red;
background: green;
}
`}
</style>
</div>
);
}
ブラウザで確認した結果、styled-jsxで適用したCSSはContentコンポーネント内のpタグのみに適用されることがわかります。

Contentコンポーネント内のみにCSSを適用できるため他のコンポーネントに影響を与えません。そのため作成したコンポーネントを再利用を効率的に行うことができます。もしコンポーネントの外に影響がある場合はCSSの上書きやクラス名の重複などを心配する必要があります。
もしコンポーネント外にstyled-cssで設定したCSSを適用したい場合はglobalをstyleタグに追加することで適用できます。
function Content() {
return (
<div>
<p>ここにコンテンツが入ります。</p>
<style global jsx>{`
p {
color: blue;
}
`}</style>
</div>
);
}
Cotentコンポーネント外のpタグにもCSSが適用されていることが確認できます。

styled-jsx内で変数を利用
styled-jsxはJSXの中つまりJavaScriptの中に記述しているので変数や関数を利用することも可能です。Contentコンポーネントが親コンポーネントから受け取ったpropsの値を利用して動的に適用するCSSを変更することができます。
propsで受け取ったtypeの文字列がalertであれば文字色は赤、alertでない文字列の場合は青になるように設定しています。
function Content({ type }) { return ( <div> <p>ここにコンテンツが入ります。</p> <style global jsx>{` p { color: ${type == “alert” ? “red” : “blue”}; } `}</style> </div> ); }
<Content type="alert" />
alertを渡すと文字は赤になっています。

<Content type="warning" />
alert以外を渡すと文字は青になっています。

CSSファイルによるCSSの適用
先ほど述べたようにCSSの適用にはさまざまな方法で行うことができます。stylesディレクトリにあるglobals.cssを利用しても適用することができます。

globals.cssファイルはnext.jsのインストール時から存在する.app.jsファイルの中でimportが行われています。
stylesディレクトリに下に保存されているglobals.cssファイルを開いて.headingを追加します。
.heading {
color: green;
}
index.jsのh1タグにはheadingクラスが設定されています。
export default function Home() {
return (
<div>
<h1 className="heading">Hello Next.js</h1>
</div>
);
}
ブラウザで確認するとglobals.cssに記述したheadingが適用されていることが確認できます。

_app.jsではなくindex.jsファイルでglobals.cssファイルをimportするとエラーが発生します。Global CSS cannot be imported from files other than your Custom . Please move all global CSS imports to pages/_app.tsx….
ではコンポーネントにCSSファイルを使ってCSSを適用したい場合はどのように行うのでしょうか。適用方法について確認を行っていきます。
module.cssファイルによりCSSの適用
stylesディレクトリの下にはNext.jsをインストール後、2つのファイルが保存されています。一つがglobals.cssでもう一つがHome.module.cssです。cssの拡張子の前にmoduleが入っているのがポイントです。
globals.cssが_app.jsからimportしてアプリケーション全体に記述したCSSを適用することができます。XXXX.module.cssファイルはコンポーネントでimportを行うことがファイルです。しかし、importの方法が異なります。
import styles from "../styles/Home.module.css";
適用方法も下記のように行います。
import styles from “../styles/Home.module.css”; export default function Home() { return ( <div> <h1 className={styles.heading}>Hello Next.js</h1> </div> ); }Home.module.cssファイルは下記のように記述しています。
.heading {
background-color: red;
}

まとめ
外部からのデータの取得方法まで確認を行うことができたので、ブログの記事データなどfetchコマンドで取得することが可能であればここまでの知識でブログサイトの作成も行うことが可能です。しかしSEOのためメタタグの設定等が必要になるため、さらに学習が必要となります。今後本ブログでも設定方法を紹介していきます。