📌

Next.jsの基礎知識

2022/02/25に公開

概要

Next.js に関する基礎知識を解説してみます。

Next.js のメリット

Server-side Rendering(SSR)のビルトイン

通常の React アプリの場合、HTML を表示すると<div id="root"></div>といったエントリポイントになる HTML のみが表示されます。
React はクライアントサイドライブラリなので、全てのレンダリングはブラウザ上で行われるためです。
画面をリロードすると一瞬白いページが見えてちらつきが発生します。
また、SEO の観点では、検索エンジンは空のページのみを返してしまい検索エンジンに取り込まれない問題があります。
SSR を用いることで、サーバ側がその URL の表示に対して生成された HTML をレスポンスすることができます。
Next.js には SSR 機能が組み込まれています。

File-based Routing

React では URL が切り替わる時、ブラウザの標準動作を抑止し、別のコンポーネントを表示させる動きをします。この場合、React ではコンポーネント用のファイルを用意し、App.js内に React Router を用いて、URL に対するルーティングを記載しますが、Next.js ではファイルの配置ルール/命名規則によって表現します。

URL に関する動きの差異整理

  • React のみ:URL によるページだし分けの概念がない。HTML はエントリポイントのタグのみ。
  • React+ReactRouter:URL によりページが出しわけられる。HTML はエントリポイントのタグのみ。
  • Next.js:URL によりページが出しわけられる。URL 直打ちするとレスポンスに HTML が含まれている。

Fullstack Capabilities

node.js のバックエンドのコード追加が可能になります。
データベースへのアクセスや認証などの機能を追加できるようになります。

Next.js の開始

ディレクトリ構成

create next-appすると以下のようなディレクトリ構成になります。
React の場合と大きく異なるのが、File-based Routingのために、pagesディレクトリ配下に公開 URL に合わせてファイルを配置していく形となっている点です。

.
┝ node_modules
┝ pages
│ ┝ api
│ ┝ _app.js
│ └ index.js
┝ public
┝ styles
┝ .gitignore
┝ package-lock.json
┝ package.json
└ README.md

URL との紐づき(pages 配下)

例えばpages/news.jsにファイルを配置すると、ブラウザで/newsにアクセスするとページが表示されます。
pages/news/index.jsであれば/news/pages/news/test.jsであれば/news/testでページが表示されます。

動的 URL

例えばニュースやブログの詳細ページのように、1 ページずつ個別のページを作るのではなくて動的にページが生成される場合、pages/[newsId].jsといった形で、ファイル名に[]に囲まれた名前を付けることで、URL から動的にパラメータを受け取ることができるようになります。

動的に受け取ったパラメータは、次のようなコードで JS の処理内で受け取ることができます。

pages/[newsId].js
import { useRouter } from "next/router";

function DetailPage() {
  const router = useRouter();
  // パラメータを受け取る
  const newsId = router.query.newsId;
  console.log(newsId);
  return <h1>The Detail Page</h1>;
}
export default DetailPage;

Client Side Navigation

通常 Web サイトでは a タグで<a href="xx">リンク</a>のようにリンクを作成しますが、この場合、ブラウザは画面遷移するたびに画面を読み込みなおします。
これはユーザ体験としてはあまり良いものではなく、画面の一部のみ変更が必要な場合は、(URL についても、見た目上変化させつつ)再読み込みせずに次の画面に遷移できることが望ましいです。
さらに、画面遷移してからその URL で画面をリロードしたとしても、正しくページが返ってくる必要があります。これは、Client Side Navigationという技術で実現できます。
なぜ、再読み込みしないのに画面が遷移できるかというと、Code splitting(JS コードの自動分割)とprefetching(コンポーネントの事前生成)という技術を用いて、実際にユーザが操作するよりも前に遷移先のコンポーネントを裏側で生成しているためです。
具体的にはLinkコンポーネントを用いて次のように実装します。

javascript(ファイル先頭)
import Link from "next/link";
javascript(リンク箇所)
<Link href="xx">リンク</Link>

これにより、画面を再読み込みせずに画面遷移できるようになります。(Next.js で管理されている内部リンクのみです)

コンポーネントへの props での値の受け渡し

親コンポーネントから子コンポーネントに対して、propsを用いて値の引き渡すを行うことができます。
例えば、ニュース一覧を表示するNewsというコンポーネントを作成し、そこにニュース一覧のリスト配列を引き渡す場合、コンポーネントの呼び出しタグに属性として変数を渡すことで、子コンポーネントに渡すことができます。

index.js
import News from "../components/News";

const news = [
  {
    id: "1",
    title: "test1",
    content: "texttext1",
  },
  {
    id: "2",
    title: "test2",
    content: "texttext2",
  },
];

function HomePage() {
  return <News news={news} />;
}

export default HomePage;
News.js
function News(props) {
  return (
    <div>
      <ul>
        {props.news.map((item) => {
          return (
            <li>
              {item.id}:{item.title}:{item.content}
            </li>
          );
        })}
      </ul>
    </div>
  );
}

export default News;
出力されるHTML
<div>
  <ul>
    <li>1:test1:texttext1</li>
    <li>2:test2:texttext2</li>
  </ul>
</div>

コンポーネントへの props での関数の受け渡し

コンポーネントに対しては値だけでなく関数も引き渡すことができます。
親コンポーネント側で関数処理を定義しておき、同じように props を用いて関数を渡しています。
子コンポーネント側でその関数を呼び出すイベントが発生すると、親コンポーネント側で定義した関数が実行されます。

index.js
import TestForm from "../../components/TestForm";

function FormPage() {
  // 子コンポーネント側に引き渡す関数の定義
  function sendData(enteredData) {
    // 子コンポーネント側から関数が呼ばれると実行される処理
    console.log(enteredData);
  }
  return <TestForm sendData={sendData} />;
}

export default FormPage;
TestForm.js
import { useRef } from "react";

function TestForm(props) {
  // インプット要素への参照をuseRef Hooksを用いて定義
  const titleInputRef = useRef();
  const contentInputRef = useRef();

  // formのsubmitが行われると呼ばれる関数を定義
  function submitHandler(event) {
    event.preventDefault();

    // インプット要素から値を取得
    const enteredTitle = titleInputRef.current.value;
    const enteredContent = contentInputRef.current.value;

    const contactData = {
      title: enteredTitle,
      content: enteredContent,
    };

    // 親コンポーネントからpropsで渡された関数を実行
    props.sendData(contactData);
  }

  return (
    <form onSubmit={submitHandler}>
      <div>
        <label htmlFor="title">Title</label>
        <input type="text" required id="title" ref={titleInputRef}></input>
        <label htmlFor="content">Content</label>
        <input type="text" required id="content" ref={contentInputRef}></input>
        <input type="submit" />
      </div>
    </form>
  );
}

export default TestForm;

CSS Modules の利用

コンポーネントに対してスタイルシートを適用する場合、そのままclassを記載するのではなく、className属性に対して設定を受け渡す形になります。
また、その際には、CSS をモジュールのように引き渡すことができる、CSS Modules を利用することができます。
CSS Modules は、*.module.cssといったファイル名で CSS 定義を用意しておき、それをオブジェクトとしてimportして、CSS のクラス情報を、オブジェクトの属性として扱い、そのままコンポーネントのclassNameに引き渡すことができます。
次のような使い方になります。

test.module.css
.text {
  color: red;
}
sample.js
import cssTest from "../styles/test.module.css";

function SamplePage() {
  return <p className={cssTest.text}>サンプル文字列</p>;
}

export default SamplePage;

上記により、<p class="text">を設定したのと同じ状態になり、実際のスタイル(文字色)も適用されます。

この CSS Module の.textといったクラスは、モジュール化されており(=他と分けて制御されるようになっており)、名前の衝突によって予期しないスタイル崩れなどが起こらないようになっています。(クラス名が衝突しないように自動制御されます)

この技術によって、コンポーネント内でスタイルを独立性をもって管理/制御できるようになっています。

Layout コンポーネントの利用

コンポーネントは、コンポーネントタグで子要素を囲むことで、他のコンポーネントを巻き付けるような形で利用することができます。
その場合、propsには、特別な変数props.childrenの中に子要素が格納された状態で、コンポーネントが呼び出されます。
次のように利用することができます。

レイアウト用のコンポーネント

Layout.js
function Layout(props) {
  return (
    <div>
      <h1>Header</h1>
      <main>{props.children}</main>
      <h2>Footer</h2>
    </div>
  );
}

export default Layout;

レイアウトを利用するページコンポーネント

sample.js
import Layout from "../components/layout/Layout";

function SamplePage() {
  return (
    <Layout>
      <div>Content</div>
    </Layout>
  );
}

export default SamplePage;
出力されるHTML
<div>
  <h1>Header</h1>
    <main>
      <div>Content</div>
    </main>
  <h2>Footer</h2>
</div>

ルートコンポーネント(_app.js)の利用

全てのページに対して共通的な設定を行う場合、クライアント処理を行う、特殊な共通コンポーネント(ルートコンポーネント)のpages/_app.jsを用いて制御を行います。

pages/_app.js
import "../styles/globals.css";

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;

ルートコンポーネント(_app.js)は、ページナビゲーション(遷移)が行われるたびに、対象ページのコンポーネント一式がComponentにセットされ、ページに渡されたpropspagePropsに設定された状態で必ず呼び出されます。
そのため、このルートコンポーネントに対して全体共通で適用する Layout 等を適用しておくと、すべてのページコンポーネントで共通的に呼び出される構成を作ることができます。

Layout.js
function Layout(props) {
  return (
    <div>
      <h1>Header</h1>
      <main>{props.children}</main>
      <h2>Footer</h2>
    </div>
  );
}
sample.js
function SamplePage() {
  return <p>ページコンテンツ</p>;
}

export default SamplePage;
出力されるHTML
<div>
  <h1>Header</h1>
    <main>
      <p>ページコンテンツ</p>
    </main>
  <h2>Footer</h2>
</div>

head タグ内の情報を Head コンポーネントで設定する

各ページのtitlemeta descriptionなど、通常headタグ内に出力する内容は、<Head>コンポーネントを用いることで、各ページに設定することができます。

設定方法は次のようになります。

index.js
import Head from "next/head";

function HomePage() {
  return (
    <>
      <Head>
        <title>This is Title</title>
        <meta name="description" content="This is description" />
      </Head>
      <h1>Hello World</h1>
    </>
  );
}

export default HomePage;

Router push/replace を用いた画面遷移処理呼び出し

画面遷移するときは、<Link>コンポーネントでページナビゲーションすることができますが、処理内でリダイレクトのように画面遷移する場合、next/routerを用いて画面遷移することができます。

router.push を用いた例

sample.js
import { useRouter } from "next/router";

function SamplePage() {
  const router = useRouter();

  // ボタンを押下したら、/news/ページに遷移させる
  function movePageHandler() {
    router.push("/news/");
  }

  return (
    <button type="button" onClick={movePageHandler}>
      Click me
    </button>
  );
}

export default SamplePage;

この場合、ブラウザのwindow.historyに追加pushがされます。つまり、ブラウザの戻るボタンに履歴が追加され、戻るボタン押下時にこのページに戻ってくることができます。(リダイレクトに近いです)

一方で、router.replaceを用いると、履歴を置換するので、このページにはブラウザの戻るボタンで戻って来られません。(このページに遷移した履歴はなかったことになります。)(イメージ的にはフォワード処理に近いです)

router.replace を用いた例

sample.js
import { useRouter } from "next/router";

function SamplePage() {
  const router = useRouter();

  // ボタンを押下したら、/news/ページに遷移させる
  // ブラウザの履歴が上書かれるので、戻るボタンでこのページには戻ってこられない
  function movePageHandler() {
    router.replace("/news/");
  }

  return (
    <button type="button" onClick={movePageHandler}>
      Click me
    </button>
  );
}

export default SamplePage;

なお、外部ページに飛ぶ場合は、window.location.hrefで飛ぶことと同義です。

処理内で起こす画面遷移アクションの整理

  • router.push:ブラウザに履歴を残しつつページナビゲーションで遷移する。
  • router.replace:ブラウザに履歴を上書いてページナビゲーションで遷移する。
  • window.location.href:ページごとリロードして遷移する。

useState と useEffect を用いて非同期に画面を更新する

外部の API などからデータを取得してきて表示することはよくあると思います。
以下の例では、useStateuseEffectを持ちいて、画面の初期表示直後に、ニュース一覧データを取得して、画面を更新しています。
const [news, setNews] = useState([])で、空の配列[]を変数newsに設定し、news変数の状態監視を開始します。
useEffect(() => { setNews(dummyNewsList); },[])で、最初のレンダリングが終わった直後に、setNews関数を呼び出しています。
useEffectは初回レンダリング時及び、レンダリングが更新される都度、その直後に後追いで呼ばれますが、第 2 引数に、2 回目以降のレンダリング時に関数が呼ばれる条件として監視する変数を設定できます。つまり、[]指定は、初回レンダリングのみアクションを起こすことを意味します。

index.js
import { useState, useEffect } from "react";
import News from "../components/News";

const dummyNewsList = [
  {
    id: "1",
    title: "test1",
    content: "texttext1",
  },
  {
    id: "2",
    title: "test2",
    content: "texttext2",
  },
];

function HomePage() {
  const [news, setNews] = useState([]);

  useEffect(() => {
    setNews(dummyNewsList);
  }, []);

  return <News news={news} />;
}

export default HomePage;
News.js
function News(props) {
  return (
    <div>
      <ul>
        {props.news.map((item) => {
          return (
            <li>
              {item.id}:{item.title}:{item.content}
            </li>
          );
        })}
      </ul>
    </div>
  );
}

export default News;

この場合、サーバ側から SSR でレスポンスされた瞬間には以下のような HTML になっています。

html(画面表示直後)
<div>
  <ul>
  </ul>
</div>

このレンダリングが行われた直後に、useEffectが動作し、次のような状態にレンダリングの更新が 1 度だけ行われます。

html(再レンダリング後)
<div>
  <ul>
    <li>1:test1:texttext1</li>
    <li>2:test2:texttext2</li>
  </ul>
</div>

これは、見た目上画面表示はされますが、SEO 的には検索エンジンにインデックスされないため、あまり望ましい結果ではありません。(後追いでレンダリングすることが悪いわけではなく、検索エンジンに読み込ませたいコンテンツの場合には、向いていないという意味です)

Page Pre-Rendering を用いてサーバサイドで事前にレンダリングしておく

Next.js の重要な機能となりますが、上記のような外部からデータ取得した場合の SEO とレンダリングの問題を解決するために、主に以下の選択肢が存在します。
なお、これは常にページ全体といったことはなく、混在することができます。

  1. Static Generation(静的生成)
  2. Server-side Rendering(サーバサイドレンダリング)

上記はレンダリングが行われるタイミングが異なります。

Static Generation(静的生成)

アプリケーションがビルドされるタイミングで 1 度だけ処理が呼ばれ、それ以降は生成されたものを参照し続けます。
更新する場合は、アプリケーションを再ビルドする必要があります。

Server-side Rendering(サーバサイドレンダリング)

サーバサイド処理が呼ばれるタイミングで都度処理が実行されます。

静的生成のために getStaticProps を使う

ページコンポーネントでは、getStaticPropsを用いることで、ビルドプロセス時にのみデータを取得することが可能です。
getStaticPropsで定義した処理は、都度表示時には呼ばれることはなく、また、クライアント側にはコードも公開されません。
事前に外部 API や DB などからデータを取得しておき、都度表示時にはその結果のみを使用して処理を行います。
また、getStaticPropspropsを返却returnしており、それは、ページコンポーネントの引数propsとして、HomePage(props)のようにページ初期表示処理に引き渡されます。

index.js
import { useState, useEffect } from "react";
import News from "../components/News";

function HomePage(props) {
  return <News news={props.news} />;
}

export async function getStaticProps() {
  const dummyNewsList = [
    {
      id: "1",
      title: "test1",
      content: "texttext1",
    },
    {
      id: "2",
      title: "test2",
      content: "texttext2",
    },
  ];

  return {
    props: {
      news: dummyNewsList,
    },
  };
}

export default HomePage;

これによって、ブラウザ表示時には、最初から HTML が組み込まれた状態になり、また、(再ビルドまでは)都度データ取得処理が動くこともありません。

html(初期表示時点 ※再レンダリング無し)
<div>
  <ul>
    <li>1:test1:texttext1</li>
    <li>2:test2:texttext2</li>
  </ul>
</div>

ただし、これにはまだ課題があり、ビルドしてから時間がたつとデータが古くなってしまう可能性があるという点です。

revalidate で指定した秒数ごとにキャッシュを更新する

getStaticPropsrevalidateオプションをつけることで、再ビルドを待たずに、指定秒数以上経過したアクセスをトリガとして、getStaticPropsのデータを更新します。

その場合、getStaticPropsreturnrevalidate: 10の設定をつかします。数字は秒数でこの設定は 10 秒キャッシュを使うことを意味します、1 時間キャッシュを使う場合は3600になります。
この値は取得するデータの性質によって変える必要があります。

index.js
import { useState, useEffect } from "react";
import News from "../components/News";

function HomePage(props) {
  return <News news={props.news} />;
}

export async function getStaticProps() {
  const dummyNewsList = [
    {
      id: "1",
      title: "test1",
      content: "texttext1",
    },
    {
      id: "2",
      title: "test2",
      content: "texttext2",
    },
  ];

  return {
    props: {
      news: dummyNewsList,
    },
    revalidate: 10,
  };
}

export default HomePage;

Server-side Rendering で getServerSideProps を用いてサーバサイドのみで都度処理をする

getServerSidePropsを用いることで、リクエストがある都度サーバサイドで実行する処理を記載することができます。
getServerSidePropsの中ではcontextを取得でき、req や res といった、リクエスト/レスポンスに関するオブジェクトも扱うことができます。

index.js
import { useState, useEffect } from "react";
import News from "../components/News";

function HomePage(props) {
  return <News news={props.news} />;
}

export async function getServerSideProps(context) {
  const req = context.req;
  const res = context.res;
  const dummyNewsList = [
    {
      id: "1",
      title: "test1",
      content: "texttext1",
    },
    {
      id: "2",
      title: "test2",
      content: "texttext2",
    },
  ];

  return {
    props: {
      news: dummyNewsList,
    },
  };
}

export default HomePage;

一見非常に便利なのですが、ただ、SSR が本当に必要な場面であるかはよく考える必要があります。(要するにやたらに使うべきでないです)
なぜなら、サーバサイドでレンダリングする意味は、SEO 対策として検索エンジンにインデックスさせるためであり、認証状態やリクエスト、Cookie などによって動的に変化するマイページのようなページは、そもそも SEO 対策をする意味がない可能性が高いためです。
Next.js においてはクライアントサイドレンダリングが React の仕組みで普通に行えるので、認証が必要な API などは、クライアントサイド側で呼んで(つまり、getStaticPropsgetServerSideProps以外の場所で、hooks 等を用いて呼んで)クライアントサイドでだし分けをすれば、たいていの場合は問題がないためです。

動的 URL のページを getStaticPaths と getStaticProps で事前に静的生成する

URL がデータによって動的に変化するページ、例えばニュース詳細ページやブログの詳細ページにおいて、Static Generation(静的生成)を用いるために、事前に動的 URL の一覧を作成(getStaticPaths)して、ビルド時に事前に静的生成を行っておきます。
全ページを事前に静的生成しておき、再ビルドまで更新しない場合は、getStaticPathsreturnfallback=falseを指定します。
また、ビルド後にページが追加された場合(=事前生成されていない動的パラメータでページが呼ばれた場合)、getStaticPathsreturnfallback=trueを指定することで、差分のページについて随時getStaticPropsを実行することもできます。

なお、動的 URL にする場合、pages/[newsId]/index.jsのようにファイルパスの中に[]で囲ったパラメータ文字列を含めておくことで、JS 処理側でparamとして取得できます。

動的 URL の対象ページをビルド時に全ページ作成しておく場合

pages/[newsId]/index.js
function NewsDetail(props) {
  return (
    <div>
      <div>{props.id}</div>
      <div>{props.title}</div>
      <div>{props.content}</div>
    </div>
  );
}

export async function getStaticPaths() {
  return {
    fallback: false,
    paths: [
      {
        params: {
          newsId: "n1",
        },
      },
      {
        params: {
          newsId: "n2",
        },
      },
    ],
  };
}

export async function getStaticProps(context) {
  const newsId = context.params.newsId;

  const newsData = {
    id: newsId,
    title: "title1",
    content: "text",
  };

  return { props: newsData };
}

export default NewsDetail;

動的 URL の対象ページがビルド時に増えた場合随時ページ作成(static generation)する場合

pages/[newsId]/index.js
function NewsDetail(props) {
  return (
    <div>
      <div>{props.id}</div>
      <div>{props.title}</div>
      <div>{props.content}</div>
    </div>
  );
}

export async function getStaticPaths() {
  return {
    fallback: false,
    paths: [
      {
        params: {
          newsId: "n1",
        },
      },
      {
        params: {
          newsId: "n2",
        },
      },
    ],
  };
}

export async function getStaticProps(context) {
  const newsId = context.params.newsId;

  const newsData = {
    id: newsId,
    title: "title1",
    content: "text",
  };

  return { props: newsData };
}

export default NewsDetail;

ここに、先述のgetStaticPropsreturnでのrevalidate指定を組み合わせることで、ページの生成や更新のタイミングをコントロールすることができます。

ページから呼び出す API を作る

Next.js では、/api/フォルダに配下に API を作成することができます。
このコードは完全にサーバ側で実行され、クライアント側に公開されることはありません。
そのため、DB や API への非公開の接続情報などを扱うことができます。
(クライアントサイド側の処理に秘匿情報を書いてはいけません)

API では以下のような形で記載を行えます。

pages/api/hello.js
export default function handler(req, res) {
  res.status(200).json({ name: "Hello World!" });
}

この API のエンドポイント URL は/api/helloとなります。

また、クライアント側では、その API を呼び出す形で処理を記載します。
たとえば、フォームに入力された値を API に送信する処理は次のような実装になります。

フォーム画面のページコンポーネント

pages/form/index.js
import TestForm from "../../components/TestForm";

function FormPage() {
  async function sendData(enteredData) {
    const response = await fetch("/api/post", {
      method: "POST",
      body: JSON.stringify(enteredData),
      headers: {
        "Content-Type": "application/json",
      },
    });
    const data = await response.json();
    console.log(JSON.stringify(data));
  }
  return <TestForm sendData={sendData} />;
}

export default FormPage;

入力フォームコンポーネント

components/TestForm.js
import { useRef } from "react";

function TestForm(props) {
  // インプット要素への参照をuseRef Hooksを用いて定義
  const titleInputRef = useRef();
  const contentInputRef = useRef();

  // formのsubmitが行われると呼ばれる関数を定義
  function submitHandler(event) {
    event.preventDefault();

    // インプット要素から値を取得
    const enteredTitle = titleInputRef.current.value;
    const enteredContent = contentInputRef.current.value;

    const contactData = {
      title: enteredTitle,
      content: enteredContent,
    };

    // 親コンポーネントからpropsで渡された関数を実行
    props.sendData(contactData);
  }

  return (
    <form onSubmit={submitHandler}>
      <div>
        <label htmlFor="title">Title</label>
        <input type="text" required id="title" ref={titleInputRef}></input>
        <label htmlFor="content">Content</label>
        <input type="text" required id="content" ref={contentInputRef}></input>
        <input type="submit" />
      </div>
    </form>
  );
}

export default TestForm;

フォーム入力内容を受け取って処理する API

api/post.js
export default function handler(req, res) {
  if (req.method === "POST") {
    const data = req.body;

    // DBやAPIに接続/参照/更新などをこなうプログラムを記載

    res.status(200).json({ result: "OK" });
  }
}

DB への接続情報など秘匿情報を使用できる個所(サーバサイドのみで動く箇所)の整理
コード内で主に以下の箇所はサーバサイドのみで動作します。

  • getStaticPaths()
  • getStaticProps()
  • getServerSideProps()
  • /api/配下のサーバサイド API 処理

本番用にプロジェクトをビルドする

実際にプロジェクトが完成したら、本番用にビルドを行い開始します。

本番用にビルドする
以下のコマンドで本番用にビルドできます。
静的ファイル生成などが行われます。

shell
yarn build

本番用サーバを開始する
以下のコマンドでビルドしたプロジェクトを開始します。

shell
yarn start

まとめ

React、Next.js は現在大変速いペースで機能追加され進化しています。

フロントエンドで非常に人気が高くなっていますが、
常に情報をアップデートしないと、すぐに情報が古くなってしまいますね。

今後の動向にも注目していきたいと思います。

株式会社トッカシステムズ

Discussion