入門者は基礎からしっかり理解Next.jsの基本(React)
本文書はNext.jsのバージョン12を元に作成していますが新たにNext.jsのバージョン13に対応した記事を公開しました。
Next.jsはオープンソースのReactベースのフロントエンドフレームワークです。パフォーマンス、SEOやアプリケーションの開発の効率化に関わるFile System Based Routing, Server Side Rendering(SSR), Static Site Generator(SSG), Incremental Static Regeneration(ISR), Image Optimization, Code Splitting, Pre-fetching, Serverless Functions, Fast Refreshなどの機能が事前に組み込まれています。これらの機能を自分で実装しようとすると非常に困難です。しかし自分で実装することができない機能であってもNext.jsを理解し使いこなすことができればアプリケーションのコードの作成に集中することができます。
Next.jsは現在も進化を続けておりバージョンをアップする度に新たしい機能が追加されています。
本文書では、これまで一度もNext.jsを触ったことがないけれど興味があるまた今後使ってみたいという人を対象にNext.jsの基本的な機能について説明を行っています。現在の最新バージョンは13ですが本文書はバーション12.3の時に執筆しています。
本文書を読み終えると下記のことを理解することができます
- Next.jsのインストール方法
- 静的ファイルの作成方法
- 動的ファイルの作成方法
- ページ間のリンクの設定方法
- _app.jsへのレイアウト設定
- 外部からのデータ取得と表示(getStaticProps, getServerSideProps)
- styled-jsxによるCSSの適用方法
- Tailwind CSSによるCSSの適用方法
Next.jsをインストールするためには事前に Node.jsのインストールを完了しておく必要があります。
動作確認の環境はmacOS Big Sur バージョン11.6.9、Next.jsのバージョンは12.3です。サポートしているOSは、MacOS, Windows (including WSL), and Linuxです。WindowsでもNode.jsをインストールすることでプロジェクトを作成することができます。
Vercelへのデプロイ方法は下記の文書で公開していますのでNext.jsで構築したサービスの公開も簡単に行うことができます。
Next.jsの認証に利用できるNextAuth.jsについては別記事として公開しています。
目次
Next.jsのインストール
npxコマンドを利用してnext.jsのインストールを行います。(yarn, pnpmも利用できます)
% npx create-next-app@latest
Need to install the following packages:
create-next-app
Ok to proceed? (y) y
✔ What is your project named? … my-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開発サーバの起動
インストールが完了するとコマンドを実行した場所にプロジェクト名のディレクトリが作成されます。
//略
└─ yocto-queue@0.1.0
✨ Done in 8.48s.
Initialized a git repository.
Success! Created my-app-next at /Users/mac/Desktop/my-app-next
プロジェクトディレクトリmy-appに移動して開発サーバを起動するためにnpm run devコマンドを実行します。コマンド実行後のメッセージを確認するとhttp://localhost:3000でサーバが起動していることがわかります。
% cd my-app
% npm run dev
> my-app-next@0.1.0 dev
> next dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
event - compiled client and server successfully in 2.1s (172 modules)
ブラウザを起動してhttp://localhost:3000にアクセスするとNext.jsの初期画面が表示されます。これでNext.jsのインストールは完了です。
プロジェクトの中にあるpackage.jsonファイルを確認すると開発中に利用することができるコマンド(npm run devなど)やインストールしたNext.jsのバージョンを確認することができます。
{
"name": "my-app-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "12.3.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"eslint": "8.23.1",
"eslint-config-next": "12.3.0"
}
}
Hello Next.js
ディレクトリ構成
プロジェクト直後のディレクトリ構成の確認を行います。エディターにはVisual Studio Code(VS Code)を利用しています。プロジェクト作成直後から存在するディレクトリはpages, styles, node_modules, publicの4つです。その他にREADME.mdとpackage.jsonとpackage-lock.jsonの3つのファイルが存在します。先頭に.(ドット)が含まれる.nextディレクトリ、.gitignoreファイルもあります。
ブラウザで表示されたページの内容はpagesディレクトリの中のindex.jsファイルに記述されています。このディレクトリの中にアプリケーションのコアとなるコードを記述していくことになります。
publicディレクトリの中には、favicon.ico, vercel.svgファイルが保存されています。これらのファイルはブラウザから直接アクセスすることができます。
ブラウザのURLのhttp://localhost:3000の後ろに/vercel.svgを追加してください。Vercelのロゴがブラウザ上に表示されます。publicディレクトリに保存したファイルには直接ブラウザからアクセスできることが確認できました。このことからpublicには静的なファイルを保存できることがわかります。例えばabout.htmlファイルをpublicディレクトリに作成するとブラウザから直接アクセスすることができ、about.htmlファイルの内容を表示させることも可能です。
stylesディレクトリの下にはCSSのファイルを保存します。CSSファイルについてはpublicディレクトリ下にcssファイルを作成して利用することもできます。publicディレクトリを利用した場合はJavaScriptのバンドルとしてではなくlinkタグで直接ファイル名を指定することでCSSを適用することになります。
index.jsファイルの更新(Fast Refresh)
pagesディレクトリにあるindex.jsファイルを更新するとブラウザ上に表示される内容も変更されるのか確認を行います。
index.jsの中身を一度削除して下記のように更新します。
export default function Home() {
return <h1>Hello Next.js</h1>;
}
npm run devを実行していればindex.jsを更新するとブラウザに自動で更新内容が反映されます。これはNext.jsが持つFast Refreshという機能です。ページをリロードすることなく更新が即反映されるので開発者の効率化につながる非常に便利な機能です。
ブラウザに表示されている内容だけではなくNext.jsのページ表示に関する動作を確認するためにページのソースを確認してみましょう。ブラウザ上で右クリックを行いページのソースを表示してください。(Chromeの場合)
ページのソースの中に”Hello Next.js”の文字列が入っていることがわかります。index.jsはJavaScriptファイルなので通常ではJavaScriptファイルをブラウザが受け取ってJavaScriptファイルを処理してその内容を画面に描写します。ブラウザのページのソースを見る限りブラウザが直接HTMLの情報(h1タグのHello World)を受け取っていることがわかります。これはNext.jsではブラウザに送信する前にサーバ側でPre-Renderingを行っているからです。HTMLの情報をそのまま受け取っているので”Hello Next.js”を表示するためにブラウザ側でJavaScriptの処理を行う必要がありません。デフォルトではNext.jsはすべてのページでPre-Renderingを行います。Pre-Renderingはクライアント側のJavaScriptで処理を行う前にNext.js側(サーバ)でページを事前に作成し作成したページをクライアントに送信する機能です。
しかしこれだけの情報ではPre-Renderingが行われているかはわかりにくいと思うのでReactと比較して違いを見てみましょう。
ReactではApp.jsファイルにh1タグで”Hello React”と記述しています。ブラウザで”Hello React”が表示されていることを確認し、ページのソースを確認します。下の画像は字が細かすぎて見えないかもしれませんがReactの場合はページのソースのどこにもh1タグは表示されていません。つまりReactではブラウザが受け取った情報にはh1タグが記述されておらず受信したJavaScriptファイルをブラウザが処理することでh1タグとその内容を描写していることがわかります。ブラウザ(=クライアント)側で処理を行うためClient Side Rendering(クライアントサイドレンダリング)と呼ばれます。
ブラウザのページのソースを比較することでClient Side RenderingとPre-Renderingの違いを理解することができました。Next.jsではPre-Renderingの方法に3つの方法があります。Static Site Generation(SSG)、Incremental Static Regeneration(ISR)、Server Side Rendering(SSR)の3つです。本文書でもそれぞれのレンダリングについて確認していきます。
別のページを作成する
Next.jsではpagesディレクトリにJavaScriptファイル(XXX.js)を作成するだけで自動でルーティングが設定されるため簡単にページを追加することができます。これはFile System Based Routingという機能です。
pagesディレクトリの下にabout.jsファイルを作成します。index.jsと同様に下記のコードを記述します。h1タグの中身と関数名のみ変更しています。
export default function About() {
return <h1>About Page</h1>;
}
ブラウザでhttp://localhost:3000/aboutページにアクセスするとAbout Pageが表示されます。Next.jsを利用するとルーティングの設定が自動で行われるため簡単にページを作成することができることが確認できました。
Next.jsではReact Routerを利用していませんがルーティングが何かわからない人は下記が参考になります。最新版はReact Router V6です。
About.jsの記述方法については関数の表示方法がいろいろあるため好きな記述方法で作成してください。
function About() {
return (
<h1>About Page</h1>
)
}
export default About
// Arrow function
const About = () => {
return (
<h1>About Page</h1>
)
}
export default About
をインストールしている場合はrafceまたはrcfeを打った後にTabキーを押すと関数のテンプレートが表示されます。
ここまでの設定では”/”(ルート)と/aboutページ以外にアクセスを行うと404エラーが表示されます。
pagesディレクトリ内にさらにディレクトリを作成しその下にjsファイルを作成した場合の動作も確認しておきます。
まずpagesの下にproductsディレクトリを作成し、bag.jsファイルを作成します。
export default function Bag() {
return <h1>バックのページです</h1>;
}
bag.jsファイルを作成後にhttp://localhost:3000/products/bagにアクセスするとbag.jsファイルの中身が表示されます。ページの階層化も簡単に行うことができます。/products/bagの形はNested Routes(ネスト化されたルーティング)と呼ばれます。
Dynamic Routingの設定
productsは商品一覧を意味し、products/bagだけではなく/products/shoesにアクセスしてもページが表示されるように設定を行なっていきます。
bag, shoesなどそれぞれに対応したファイルbag.js, shoes.jsファイルを作成するのではなくダイナミックルーティングを利用してURLのproduct/の後ろにどんな文字列を入れてもブラウザにファイルの内容が表示されるように設定を行います。
productsディレクトリの下にスクエアブラケットで囲まれた[name].jsファイルを作成します。
export default function Name() {
return <h1>商品のページです</h1>;
}
/products/bagでもアクセス可能でbag.jsファイルの内容が表示されますが、bagの文字列をclothesやshoesに変更すると[name].jsファイルに記述した”商品のページです”が表示されます。
URLをclothesやshoesに変更しても同じ”商品のページです”が表示されるので/products/以下のURLに入っている文字列をページ内容に表示させるためuseRouter Hookを利用します。useRouter Hookを利用することでアクセスしてきたURLによって動的にページの内容を変えることができます。
useRouter Hookは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"
__proto__: Object
またURLにパラメータをつけてもrouter.queryから追加したパラメータの値を取得することが可能です。
URLにはhttp://localhost:3000/products/shoes?color=redのようにcolorのパラメータと値を設定しています。
さらにページの階層が深い場合にもダイナミックルーティングを利用することができます。
productsディレクトリの下に[name]ディレクトリを作成します。さらに[name]ディレクトリの下に[color].jsファイルを作成します。
import { useRouter } from "next/router";
export default function Color() {
const router = useRouter();
console.log(router.query)
return <h1>{router.query.name}の{router.query.color}カラーです</h1>;
}
ブラウザからhttp://localhost:3000/products/clothes/redにアクセスするとURLに含まれるclothesとredが表示されます。
[color].jsファイルは分割代入(Destructuring assignment)を利用して下記のように記述することもできます。
import { useRouter } from "next/router";
export default function Color() {
const router = useRouter();
const { name, color } = router.query
return <h1>{ name }の{ color }カラーです</h1>;
}
リンクの設定
index.js, about.jsとproducts の下に[name].jsファイルを作成しましたが、作成した各ページに対してリンクの設定を行っていないためページを移動するためにはブラウザのURLを手動で書き換える以外に方法がありません。リンクを利用してページ移動ができるようにLinkコンポーネントの設定を行います。
index.jsが表示されるルートページからaboutページへの移動を行うためLinkを利用します。Linkはnext/linkからインポートします。Linkタグの中で移動したいページをhref propsを利用して設定します。
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ページに移動する際にページのリロードは行われずスムーズにAboutページの画面が表示されます。
もしLinkコンポーネントではなくこれまでのHTMLのようにaタグを利用して設定を行ってみてください。ページの移動を行うことができますがページを移動する際にページのリロードが行われページが表示されるまでに時間がかかることがわかります。
hrefに設定する値はパスだけではなくオブジェクトを利用することができます。設定後リンクをクリックすると/about?name=testでアクセスが行われます。
import Link from 'next/link';
export default function Home() {
return (
<div>
<ul>
<li>
<Link
href={{
pathname: '/about',
query: { name: 'test' },
}}
>
About
</Link>
</li>
</ul>
<h1>Hello Next.js</h1>
</div>
);
}
移動先の情報の入った配列を利用してダイナミックルーティングを行いたい場合も下記のように設定をすることでページのリロードなしにページ遷移することができます。
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 key={product.name}>
<Link href={`/products/${product.name}`} >
<a>{product.name}</a>
</Link>
</li>
);
})}
<li>
<Link href="/about">
<a>About</a>
</Link>
</li>
</ul>
<h1>Hello Next.js</h1>
</div>
);
}
リストの項目をクリックするとクリックしたページに移動することができます。
prefetchの動作確認
Linkコンポーネントのpropsにはhref以外にもprefetchなどがありデフォルトではprefetchはtrueに設定されています。リンクがブラウザのviewport(ビューポート)に入ると自動でリンク先のJavaScriptファイルのダウンロードを行います。動作確認は開発環境(npm run dev)では行うことができないので本番環境(npm run build && npm run start)を実行して行います。npm run buildでは作成したプロジェクトをビルドします。npm run startはビルドを行い、本番サーバが起動します。
確認をする際はブラウザのデベロッパーツールのネットワークタブを利用します。Aboutページへのリンクがブラウザでアクセスした直後では表示されないようにstyle属性を利用してmargin-topを設定しています。
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 key={product.name}>
<Link href={`/products/${product.name}`}>
<a>{product.name}</a>
</Link>
</li>
);
})}
<li style={{ marginTop: '100em' }}>
<Link href="/about">
<a>About</a>
</Link>
</li>
</ul>
<h1>Hello Next.js</h1>
</div>
);
}
ページを開いてスクロールをしていくとaboutのリンクがブラウザ上に表示された瞬間にaboutXXX.jsファイルのダウンロードが行われます。これがprefetchです。
prefetchはfalseにすることができます。その場合はaboutの文字列が画面に表示されてもファイルのダウンロードは行われません。しかしリンクをhoverするとダウンロードが開始されます。falseの設定は下記のように行うことができます。
<Link href="/about" prefetch={false}>
レイアウトの設定
複数のページを持つアプリケーションを開発する場合にヘッダーやフッターなどのコンテンツは共通でメインのコンテンツのみ異なるというケースがほとんどだと思います。共通のコンテンツとメインコンテンツを分けるためにレイアウトを利用することができます。もしレイアウトを設定しない場合はヘッダーやフッターの更新を行った際にすべてのページで更新を行う必要がでてきます。そのため複数ページを持つアプリではレイアウトの設定は必須となります。
アプリケーション全体で共通のレイアウトを設定するためcomponentsディレクトリを作成してLayout.jsファイルを作成します。
import Header from './header';
import Footer from './footer';
export default function Layout({ children }) {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
);
}
componentsディレクトリにさらにheader.js, footer.jsファイルを作成します。header.jsファイルでは/(ルート)とAboutページへのリンクを設定しています。
import Link from 'next/link';
export default function Header() {
return (
<ul>
<li>
<Link href="/">
<a>Home</a>
</Link>
</li>
<li>
<Link href="/about">
<a>About</a>
</Link>
</li>
</ul>
);
}
export default function Footer() {
return (
<div>
<p>Footerコンポーネント</p>
</div>
);
}
Layout.jsファイルと関連するファイルの作成が完了したら_app.jsファイルを開いてLaytoutコンポーネントをimportして下記の設定を行います。_app.jsファイルはアプリケーションのエントリーポイントに当たるファイルですべてのページはこのファイルによってラップされています。すべてのページに対して共通となるコンポーネントを適用したい場合などに利用することができます。
import '../styles/globals.css';
import Layout from '../components/layout';
export default function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
これでレイアウトの設定は完了です。localhost:3000にアクセスすると上部に2つのリンクと フッターに設定した文字列が表示されます。
ヘッダーにあるAboutのリンクをクリックするとAboutページが表示されます。
ここでLayoutの設定を_app.jsに設定せずにindex.js, about.jsで設定を行う場合とどのような違いがあるのか疑問に思った人はいないでしょうか。つまり下記のように設定を行います。
import Layout from '../components/layout';
export default function Home() {
return (
<Layout>
<h1>Hello Next.js</h1>
</Layout>
);
}
import Layout from '../components/layout';
export default function About() {
return (
<Layout>
<h1>About Page</h1>
</Layout>
);
}
次に_app.jsファイルは元の状態に戻します。
import '../styles/globals.css';
export default function MyApp({ Component, pageProps }) {
return (
<Component {...pageProps} />
);
}
ブラウザで動作確認を行うとブラウザ上の見た目も変わらずページの遷移も問題なく行われます。_app.jsにLayoutを設定しなくても上記の設定であれば問題はありません。しかしHeaderやFooterなどのuseStateのなどの変数を持っている場合の動作で違いが明確になります。
footer.jsファイル内でuseState Hookを利用してcountを定義してボタンをクリックすると1増える機能を追加します。
import { useState } from 'react';
export default function Footer() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<div>
<button onClick={() => setCount(count + 1)}>Count+</button>
</div>
<p>Footerコンポーネント</p>
</div>
);
}
ボタンをクリックするとカウントの数字は増えていきます。
カウントが増えたところでAboutをクリックしてページを移動してください。カウントの値は0になっていることがわかります。
index.jsとabout.jsからレイアウトの設定を解除して、再度_app.jsファイルにレイアウトの設定を行ってください。
import '../styles/globals.css';
import Layout from '../components/layout';
export default function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
先ほどと同様にボタンをクリックしてカウントを増やした後にaboutでページを移動してください。countの値が0にならず保持されていることが確認できます。このように_app.jsを利用することでコンポーネントの状態が保持できることがわかりました。
Next.jsの説明では動作確認した内容について”If you only have one layout for your entire application, you can create a Custom App(.app.js) and wrap your application with the layout. Since the <Layout />
component is re-used when changing pages, its component state will be preserved (e.g. input values).”と記載されています。
ページ毎に異なるレイアウト
先ほどの設定ではすべてのページで必ず同じレイアウトが設定されてしまうことになります。index.jsとabout.jsでレイアウトを分けたい場合はgetLayoutプロパティを利用することができます。
about.jsにLayoutコンポーネントをimportしてgetLayoutプロパティに関数を設定します。
import Layout from '../components/layout';
export default function About() {
return <h1>About Page>/h1>
}
About.getLayout = function getLayout(page) {
return <Layout>{page}</Layout>
};
_app.jsを下記のように更新します。ComponentがgetLayoutプロパティを持っている場合にはabout.jsで設定したgetLayoutの関数が実行されることになり、getLayoutがなければそのままコンポーネントを戻します。
export default function MyApp({ Component, pageProps }) {
const getLayout = Component.getLayout || ((page) => page);
return getLayout(<Component {...pageProps} />);
}
設定後/aboutにアクセスするとヘッダーとフッターが表示されます。index.jsではgetLayoutを設定していないのでヘッダーとフッターが表示されることはありません。このようにgetLayoutを利用することでページ毎に異なるレイアウトを設定することができます。
admin以下のレイアウトをわける
ファイル毎にレイアウトをわけるのではなく/admin以下のURLにアクセスがあった場合に異なるレイアウトを設定した場合は下記のように設定することができます。
import Layout from "../components/layout";
import AdminLayout from "../components/AdminLayout";
import { useRouter } from "next/router";
function MyApp({ Component, pageProps }) {
const router = useRouter();
const admin = router.route.startsWith("/admin") ? true : false;
const getLayout = admin
? (page) => <AdminLayout>{page}</AdminLayout>
: (page) => <Layout>{page}</Layout>;
return (
<>
{getLayout(<Component {...pageProps} />, pageProps)}
</>
);
}
export default MyApp;
画像の表示
画像を表示する場合はImageコンポーネントを利用してブラウザ上に表示する方法を確認します。本文書ではフリー画像を利用するためにunsplash.comを利用して画像をダウンロードしてpublicディレクトリに保存します。名前はmicrosoft365.jpgで保存しています。Imageコンポーネントを利用するためにはnext/imageからimportする必要があります。propsのwidthとheightを設定します。
import Image from 'next/image';
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>
<Image src="/microsoft365.jpg" width={500} height={300} />
</div>
);
}
幅500px、高さ300pxで画像が表示されます。
publicディレクトリに保存した画像を表示させることができました。次はunsplash.comの画像のリンクを設定します。
import Image from 'next/image';
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>
<Image
src="https://images.unsplash.com/photo-1640622842223-e1e39f4bf627?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY0MjY4OTkyMQ&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080"
width={500}
height={300}
/>
</div>
);
}
ブラウザで確認するとServer Errorが表示されます。”on `next/image`, hostname “images.unsplash.com” is not configured under images in your `next.config.js` See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host”
このエラーを解消するためにはnext.sjの設定ファイルであるnext.config.jsファイルに画像のリンク先のドメイン名を登録する必要があります。
module.exports = {
images: {
domains: ['images.unsplash.com'],
},
}
next.config.jsファイルを更新した場合は再読込のためnpm run devコマンドを再実行してください。ブラウザに表示される画像はローカルの画像ではなくimage.upspash.comに保存されている画像が表示されます。
SEO メタタグの設定
検索エンジンに公開したページを見つけ上位表示してもらうためにはメタタグの設定を行う必要があります。Next.jsでメタタグを利用したい場合はnext/headを利用します。
ページのtitleの設定を行いたい場合は下記のように行うことができます。
import Link from 'next/link';
import Head from 'next/head';
export default function Home() {
return (
<div>
<Head>
<title>トップページ</title>
</Head>
<ul>
<li>
<Link href="/about">
<a>About</a>
</Link>
</li>
</ul>
<h1>Hello Next.js</h1>
</div>
);
}
ブラウザのタブには設定したtitleが表示されていることを確認できます。
ページのソースを見るとtitleタグが追加されていることも確認できます。
titleだけではなくmetaタグのdescriptionやOGPの設定もHeadタグの中で行うことができます。
<Head>
<title>トップページ</title>
<meta name="description" content="これはトップページです" />
<meta property="og:title" content="トップページ" />
<meta property="og:description" content="これはトップページ" />
</Head>
上記では値を直接設定しましたがブログの個別ページではtitleは表示するページごとに変える必要があります。通常はpropsに含まれる個別ページのメタ情報を利用して設定を行いますがここでは変数producsの値を利用して設定を行なっています。
import Link from 'next/link';
import Head from 'next/head';
const products = [{ name: 'bag' }, { name: 'shoes' }, { name: 'socks' }];
export default function Home() {
return (
<div>
<Head>
<title>{products[0].name}</title>
<meta name="description" content={`${products[0].name}のページ`} />
<meta property="og:title" content={products[0].name} />
<meta
property="og:description"
content={`${products[0].name}のページ`}
/>
</Head>
//略
外部からデータを取得(SSG, SSR)
外部から取得したデータを利用してブラウザ上に表示させる方法を確認していきます。データの取得には、JSONPlaceholderサービスを利用させてもらいます。
JSONPlaceholderを利用するとhttps://jsonplaceholder.typicode.com/postsにアクセスするだけで100件のPOSTS(投稿)データを取得することができます。またURLのpostsの後ろにID番号を入れることで個別のPOST(投稿)データにアクセスすることができます(posts/1, posts/2,…)。
Next.jsではサーバ側でデータを取得してPre-Renderingする方法には3つの方法(SSG, SSR, ISR)が存在し、ここではSSGとSSRの2つの方法を確認します。ISR(Incremental Static Regeneration)については本文書では動作確認していません。
- getStaticProps(Static Site Generation)
- getServerSideProps(Server Side Rendering)
getStaticPropsはStatic Site Generation(SSG)でビルド時に一度だけデータを取得して事前にページをpre-Renderingします。getServerSidePropsはServer Side Rendering(SSR)でクライアントからのアクセス時にサーバ側でデータを取得しPre-Renderingします。getStaticProps, getServerSidePropsはどのファイルでも実行することができるわけではなくページファイルでのみ実行でき、コンポーネントファイルで実行することはできません。
ブログの記事のように頻繁にページの追加、更新がない場合にはSSGを利用します。検索ページのようにリクエストによって表示される内容が変わる場合にはSSRを利用することができます。SSGはビルド時にHTMLページを作成してCDN(Cotent Delivery Network)にキャッシュされているのですぐにページが表示されます。SSRはリクエスト毎にサーバ上でHTMが作成されるのでページが表示させるまでに時間はかかりますが最新の情報を表示することができます。
サーバ側でしか外部リソースのデータは取得できないの?と疑問に持った人もいるかと思います。サーバ側ではなくクライアント側でもデータを取得することは可能です。Next.jsではSWR(stale-while-revalidate)というクライアント側でデータを取得する際に利用できるReact Hookライブラリが提供されているので本文書でもSWRを利用したデータ取得の方法を確認していきます。
Next.jsではサイト、アプリケーションの高速化を実現するためにSSR, SSG, ISR, SWRをうまく活用していく必要があります。
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 } };
}
通常のJavaScriptであればコード中に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>;
}
/posts/index.jsファイルに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の略でアクセスしたきたクライアント情報やヘッダー情報も確認することができます。
略
export async function getServerSideProps(context) {
console.log(context);
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const posts = await res.json();
return { props: { posts } };
}
個別ページのデータ取得
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データの内容を表示させることができます。
存在しないIDへのアクセス
URLは手動で変更が行えるためidの値を変更することが可能です。JSONPLACEHOLDERはpostsデータを100件までしか戻してくれないため/posts/101にアクセスした時に何が表示されるのか確認しておきましょう。下記のようにタイトルのみ表示されます。
console.logを使って存在しないIDにアクセスがあった場合にどのようなデータを取得しているのか確認しておきましょう。
export async function getServerSideProps({ params }) {
const id = params.post;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
console.log(post);
return { props: { post } };
}
npm run devコマンドを実行したターミナルを見ると空のオブジェクト{}であることがわかります。
getServerSidePropsを実行した時returnでpropsを持つオブジェクトを戻していましたがprops以外にもnotFoundを持つオブジェクトを戻すことができます。NotFoundはtrueとfalseの値を持つことができますがfalseに設定するとエラーになります。
return {
notFound: true,
};
存在しないIDでアクセスした空のオブジェクトになることが確認できているので、空のオブジェクトの判定でnotFoundを持つオブジェクトを戻すように設定を行います。
export async function getServerSideProps({ params }) {
const id = params.post;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
if (!Object.keys(post).length) {
return {
notFound: true,
};
}
return { props: { post } };
}
再度/posts/101にアクセスすると404ページが表示されます。
getStaticPropsを利用した方法
getServerSidePropsはリクエスト毎にサーバ側でデータの取得が行われますがgetStaticPropsはビルド時にサーバ側でデータの取得を行います。getStaticPropsで実行されるコードはクライアント側に送信されるJavaScriptのbundleコードに含まれないためクライアント側でコードの内容を確認することもできません。
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 } };
}
npm run devを実行中にファイルを保存した場合は
メソッドを変更しただけではブラウザ上では何も変化がなく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(Static Site Generator)ページでは必要と記述されています。
getStaticPathsによるパス情報の取得
エラーメッセージに表示されている通りgetStaticPathsメソッドの追加を行います。getStaticPathsのメソッドの中ではビルド時に作成するページのパス一覧を作成して、pathsでreturnする必要があります。getStaticPathsもgetStaticPropsと同様にサーバ側で実行されるためクライアント側に送信されるJavaScriptのbundleコードに含まれないためクライアント側で実行されることもコードの中身を見ることもできません。
以下がgetStaticPathsメソッドの中身です。JSONPlaceholerにアクセスしてpostsの一覧を取得し取得したpostsをmap関数で展開して個別ページのidを取り出してパス(/posts/${post.id})を作成しています。
//略
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で個別ページのパス情報が取得できると個別ページにアクセスしてもエラーなしでページが表示されます。
下記のように記述することもできます。
//略
export async function getStaticPaths() {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const posts = await res.json();
const paths = posts.map((post) => ({
params: { post: post.id.toString() },
}))
return {
paths,
fallback: false,
};
}
getStaticPathsは作成するページのパス情報を渡す役割をもっているため、getStaticPathsがない場合はパス情報がないため、先ほどのようなエラーが発生します。
fallbackの設定
fallbackはtrueとfalseを設定することができ、falseに設定した場合は存在するPostのページ以外にアクセスした場合に404エラーが表示されます。下記は存在しないidの101にアクセスを行っています。
fallbackをtureに設定した場合はサーバエラーの画面が表示されます。ページを作成使用としていますが表示しようとするプロパティのidがundefinedなのでエラーになっています。
ビルド時に存在しなかったページが追加された場合にtureを設定しておくことでそのページにアクセスするとページの作成が行われ作成されたページをユーザに返すことができます。
ページが作成されるまでにrouterのisFallbackプロパティを利用することでLoading…を画面に表示させることができます。ページが作成されると画面から”Loading…”が消え作成されたページが表示されることになります。”Loading…”が一瞬表示されるだけです。isFallbackは最初はtrueですが、ページが作成されるとfalseになり”Loading…”が消えます。
if (router.isFallback) {
return <div>Loading...</div>
}
追加されていないページにアクセスを行うと”Loading…”が表示された後に画面には”Unhandled Runtime Error Error:Failed to load static props”のエラーが表示されます。
fallbackには”blocking”を設定することもでき、blockingを設定するとページが作成されるまで待ってくれるためエラーが表示されません。isFallbackの値はblockingの場合はfalseのままなのでrouter.isFallbackを設定する必要はなくblockingの場合は画面に”Loading…”が表示されることはありません。
npm run buildコマンド実行
getStaticPropsではビルド時に静的なページが作成されるので実際にnpm run buildを実行してみましょう。実行するとリアルタイムで静的ページが生成されていることが実行のメッセージログから確認することができます。
% npm run build
//略
info - Generating static pages (104/104)
//略
作成されるファイルは.next/server/pages/postsの下に1.html, 1.jsonから100.html, 100.jsonまでの200個のファイルと[post].jsファイルが作成されることが確認できます。再度npm run devを実行すると200個のファイルは削除されpostsディレクトリには[index].jsファイルのみ残された状態になります。
1.htmlファイルの中身を確認するとブラウザに表示されるそのままの情報が含まれていることが確認できます。
SWRによるデータ取得(クライアント)
SWRはクライアント側で利用するデータ取得のためのReact HookでNext.jsを開発したVercelが提供しているライブラリです。Next.jsのドキュメントでは”The team behind Next.js has created a React hook for data fetching called SWR. We highly recommend it if you’re fetching data on the client side. ”と記載があるようにクライアント側で利用することを推奨しています。
SWRを利用するためにはライブラリのインストールが必要になります。
% npm install swr
利用方法はインストールしたswrをimportしてuseSWR Hookの第一引数にkey、第二引数にfetcher関数を設定します。keyにはURLを設定します。戻り値の中にはdata, errorが含まれています。dataだけではなくerrorも戻されるのでエラー処理も簡単に行うことができます。
import useSWR from 'swr'
//
const { data, error } = useSWR(
'https://jsonplaceholder.typicode.com/posts',
fetcher
);
fetcher関数は第一引数のURLを利用してデータ取得の処理を記述します。
const fetcher = (url) => fetch(url).then((res) => res.json());
これらの処理を利用することでposts/index.jsファイルを下記のように記述することができます。errorが戻された場合はブラウザ上にはエラーメッセージが表示されます。データの取得中にはdataの値はundefinedになるためloading…の文字列がブラウザ上に表示されます。
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
export default function index() {
const { data, error } = useSWR(
'https://jsonplaceholder.typicode.com/posts',
fetcher
);
if (error) return <div>failed to load</div>;
if (!data) return <div>loading...</div>;
return (
<div>
<h1>POST一覧</h1>
<ul>
{data.map((post) => {
return <li key={post.id}>{post.title}</li>;
})}
</ul>
</div>
);
}
動作確認を行うとJSONPlaceholderから取得した情報が表示されます。このようにSWRを利用してクライアント側でデータを取得することができます。
クライアント側でのデータ取得の処理の場合にSWRは必須ではなく通常通りuseState, useEffectなどを利用してデータを取得することは可能です。SWRはこれ以外にもuseSWRの第一引数のキーを元にデータをキャッシュしたり、一度ブラウザからカーソルを外し再度ブラウザをクリックするとデータの再取得を自動で行ったりとさまざまな機能を備えています。ブラウザのデベロッパーツールのネットワークタブを確認することでデータの再取得などが自動で行なわれることを確認することができます。
環境変数の設定
.env.localファイル
API_KEYやデータベースへの接続情報などコードに直接記述するのではなく環境変数を利用して設定したい場合は.env.localファイルを利用することができます。
プロジェクトディレクトリに.env.localファイルを作成し下記のように環境変数を設定することができます。
API_KEY=myapikey
DB_HOST=localhost
DB_USER=myuser
DB_PASS=mypassword
コード中で利用したい場合はprocess.env.API_KEY、process.env.DB_HOSTで値を取得することができます。
getServerSidePropsで利用したい場合は下記のように利用することができます。
export async function getServerSideProps() {
const api_key = process.env.API_KEY;
const result = await fetch(
`https://end_point_url/?api_key=${api_key}`
);
.env.localファイルからの値の取得はgetServerSidePropsのようにサーバ側で実行される場合には可能ですがブラウザ側の処理で利用したい場合はNEXT_PUBLIC_を追加する必要があります。
NEXT_PURLIC_API_KEY=myapikey
API_KEY=myapikey
DB_HOST=localhost
DB_USER=myuser
DB_PASS=mypassword
コードで利用する場合もprocess_env.NEXT_PUBLIC_API_KEYから取得します。
useEffect(() => {
const fetchData = async () => {
const api_key = process.env.NEXT_PUBLIC_API_KEY;
環境変数を追加し値がundefinedになっている場合はnpm run devコマンドを再実行してください。
その他の環境変数
.env.localの他に.env.development(開発環境用), env.production(本番環境用)という名前の環境変数を保存するファイルを作成することができます。もし開発環境で.env.localと.env.developmentに同じ名前の環境変数を追加し異なる値を設定した場合は.env.localによって上書きされます。.env.productionに関しても同様で.env.localによって値は上書きされます。
.env.localファイルはgitでpushしてもアップロードされないように.gitignoreファイルに指定されています。
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
.gitignoreファイルには先ほど説明したファイルとは異なる.env.development.local, .env.production.localというファイルを確認することができます。これはそれぞれ開発環境と本番環境で利用することができ、.env.localよりも設定した値は優先されます。
まとめると開発環境であれば.env.developmentl.localが一番優先度が高く.env.local, .env.developmentと優先度が下がることになります。
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のインストール時から存在するpagesディレクトリの下にある_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です。Next.jsはCSS Modulesをサポートしており、cssの拡張子の前にmoduleが入っているのがポイントです。自動でユニークなclass名を設定してくれるので別のxxx.module.cssファイルで同じ名前のclass名を利用したとしても他のファイルでは異なるclass名となるのでclass名が重複することはありません。
globals.cssが_app.jsからimportしてアプリケーション全体に記述したCSSを適用することができます。XXXX.module.cssファイルはコンポーネントでimportを行うことができるファイルです。しかし、importの方法が異なります。
import styles from "../styles/Home.module.css";
importしたstylesはオブジェクトとなり設定したclassはオブジェクトのプロパティとして登録されています。Home.module.cssファイルからimportしたstylesを利用してclassを適用したい場合には下記のように行います。
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;
}
class名にどのような名前が付けられているか確認したい場合は、console.logを利用して確認することができます。
import styles from "../styles/Home.module.css";
console.log(styles)
export default function Home() {
return (
<div>
<h1 className={styles.heading}>Hello Next.js</h1>
</div>
);
}
//
{container: 'Home_container__bCOhY', main: 'Home_main__nLjiQ', footer: 'Home_footer____T7K', title: 'Home_title__T09hD', description: 'Home_description__41Owk', …}
heading: "Home_heading___LpL1"
code: "Home_code__suPER"
container: "Home_container__bCOhY"
description: "Home_description__41Owk"
footer: "Home_footer____T7K"
grid: "Home_grid__GxQ85"
logo: "Home_logo__27_tb"
main: "Home_main__nLjiQ"
title: "Home_title__T09hD"
}
デベロッパーツールの要素を見てもheadingではなく別のclass名になっていることが確認できます。
Tailwind CSSによるCSSの適用
Next.jsでもTailwind CSSを利用することができます。Tailwind CSSのインストール手順についてはTailwind CSSのドキュメントに記載されているのでドキュメントを参考に設定を行います。
npmxコマンドでパッケージのインストールを行います。
% npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
パッケージのインストール完了後、Tailwind CSSの設定ファイルの作成を行います。
% npx tailwindcss init -p
tailwindcss 2.1.2
✅ Created Tailwind config file: tailwind.config.js
✅ Created PostCSS config file: postcss.config.js
tailwind.config.js、postcss.config.jsファイルが作成されます。
tailwind.config.jsファイルを開いてpurgeの設定を行ってください。デフォルトではpurgeの設定は[]です。
module.exports = {
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
stylesディレクトリにあるglobal.cssファイルにtailwind cssの@tailwindディレクティブを記述します。デフォルトから_app.jsファイルでglobal.cssはimportされています。
@tailwind base;
@tailwind components;
@tailwind utilities;
以上にTailwind CSSを利用するための設定は完了です。Tailwind CSSのフォントサイズのクラスであるtext-3xlを適用するとブラウザ上にはサイズの大きいHello Next.jsが表示されます。
export default function Home() {
return (
<div>
<h1 className="text-3xl">Hello Next.js</h1>
</div>
);
}
Tailwind CSSのtext-3xlクラスが設定されていない場合はnpm run devを再実行してください。
_document.jsによるカスタマイズ
Next.jsではindex.jsファイルにheadタグ、bodyタグ、scriptタグなどを記述しなくても自動で設定されています。_document.jsファイルを利用することでheadタグ、bodyタグなど全ページに共通する設定をカスタマイズすることができるようになります。
全ページへの共通設定といえばレイアウトの設定で利用した_app.jsがあります。_app.jsはbodyタグの中にのみ設定が反映されるのに対して_document.jsはhtmlタグやheadタグに対して変更を加えることができます。_document.js内でbodyタグの内部にコンポーネントの追加なども可能です。
カスタマイズを行う前に.pagesディレクトリの下に_document.jsファイルを作成してください。_document.jsファイルを作成後下記のコードを記述してください。記述している内容はNext.jsのドキュメントに記載されている内容をコピー&ペーストしているだけです。これでカスタマイズを行うための準備は完了です。記述しているコードはFunctionコンポーネントではなくClassコンポーネントです。Classコンポーネントではrenderメソッドなどを利用して記述します。
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<Main />
<div id="portal"></div>
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
HTMタグにlangを設定しています。
<Html lang="ja">
ブラウザを見ただけではlangに設定した設定値を確認することができないのでブラウザからソースを確認してください。
ソースコードを確認してhtml lang=”ja”を見つけることができれば_document.jsファイルによりカスタマイズが行われていることがわかります。
_document.jsファイルによってhtmlタグがカスタマイズを行われることがわかりましたがもし_document.jsファイルのMainタグを削除したらどうなるのかと気になる人もいるかと思います。実際に削除して確認してみましょう。
Mainタグを削除するとindex.jsファイルに記述した内容は表示されなくなります。このことからindex.jsファイルの中身はMainタグの部分に表示されることがわかります。Mainタグではなく_document.jsに記述したHeadタグ、NextScriptタグは必須なので削除したらエラーによりページが表示されなくなります。
_document.jsファイルはサーバ側で処理が行われるためクリックイベントなどを設定しても実行することはできません。そのかわりモーダルウィンドウなどを利用するためにReactのPortal機能を使いたい場合はbodyタグの閉じタグの前に<div id=”portal”></div>を追加することができます。
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
render() {
return (
<Html lang="ja">
<Head />
<body>
<Main />
<div id="portal"></div>
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
ソースを見るとbodyタグの閉じタグの前にはNextSciprtタグによりスクリプトが登録されていますが追加したdiv id=”portal”の要素は<div id=”_next”></div>タグの後ろに追加されることがわかります。
フォントの設定
Googleフォントなどを利用してアプリケーション全体にフォントを適用したい場合にも_document.jsファイルを利用することができます。
Google Fontsによって検索を行なって設定を行いたいフォントを見つけてください。ここでは人気のRobotoを選択します。表示されているRobotoをクリックして、利用したいフォントを選択してください。
ここではRegularを選択しています。
右側に表示されているラジオボタンで<link>に表示されているタグをコピーして_document.jsファイルに貼り付けます。linkタグには閉じタグの設定を行ってください。
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
render() {
return (
<Html>
<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
設定後はglobal.cssでbodyに設定されているfont-familyのrobotoを選択に持ってきます。
html,
body {
padding: 0;
margin: 0;
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI,
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
ページを見るとフォントがRobotoになっていることを確認できるはずです。global.cssが_app.jsでimportされていない場合には適用されないのでglobal.cssがimportされていることも確認する必要があります。
API Routesについて
Next.jsはフロンエンドとしてだけでなくバックエンドとしても利用できるAPI Routesという機能を備えています。API Routesを利用して作成したコードはServerless Functionsとしてデプロイされます。 API Routesの機能については別記事で公開しているのでAPI Routesを利用するとどのようなことが実現できるのか理解を深めることができます。
importのパスについて(相対から絶対)
デフォルトの設定ではコンポーネントをimportしたい場合に相対パスを利用します。
例えば_app.jsファイルからlayout.jsファイルをimportしたい場合はlayout.jsファイルがcomponentsファイルに存在するため以下のように相対パスで設定します。
import Layout from "../components/layout";
絶対パスを利用することができればimportを下記のように行うことができます。
import Layout from "@/components/layout";
JavaScriptであれば基準となる場所にjsconfig.jsonファイルを作成して以下を記述します。ファイルはプロジェクトフォルダの直下に作成します。
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"]
}
}
}
baseUrlで基準となる場所を指定します。.(ドット)を設定しているのでjsonfig.jsonファイルが存在する場所が基準となりここではプロジェクトフォルダの直下となります。pathsはエイリアスの設定でcomponentsを@/componentsと記述することができます。もしpathsの設定がない場合は@を利用することができないので以下のようにimportします。
import Layout from "/components/layout";
jsconfig.jsonファイルを更新した後はnpm run devを再実行してファイルの読み込みを行う必要があります。
まとめ
外部からのデータの取得方法まで確認を行うことができたので、ブログの記事データなどfetchコマンドで取得することが可能であればここまでの知識でブログサイトの作成も行うことが可能です。しかしSEOのためメタタグの設定等が必要になるため、さらに学習が必要となります。今後本ブログでも設定方法を紹介していきます。