Next.js(React備忘録3)

Next.jsはReact開発のための機能を提供するフレームワークです
サーバーサイドのレンダリングやルーティングが簡単に実装できます

目次
  1. Appコンポーネント
  2. ルーティング
    1. ページ遷移
  3. レンダリングについて
  4. 外部データ取得
    1. SSG
    2. ISR(Incremental Static Regeneration)
    3. SSR
    4. CSR(クライアントサイド)
  5. APIルート
  6. Netlifyに静的ファイルをデプロイする手順

Appコンポーネント

Appコンポーネントを使用して全てのページを初期化します
Appコンポーネントは./pages/_app.jsにファイルを作ることでカスタマイズできます
pages内のコンポーネントが呼ばれるとき(ページ遷移時)に必ず実行されます)
全ページ共通の処理をはここに書きます
例:styles.cssグローバルスタイルシートをimport(グローバルCSSを他の場所にインポートすることはできません)
*src/styles/:全体のスタイルを配置する

import '../styles.css'

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

状態管理
アプリケーション全体で共通するstateは「ReactContext」を使用します
プロバイダーを作成し、_app.jsの<Component />をプロバイダーでラップします

import { MyProvider } from '../context/state';
export default function MyApp({ Component, pageProps }) {
  return (
  <MyProvider>//ラップする
    <Component {...pageProps} />
  </MyProvider> )
 }
}
//プロバイダーを作成
import { createContext, useContext, useState } from "react";
const MyContext = createContext();
const MyProvider = ({ children }) => {
    const [state, setState] = useState([]);
    return (
        <MyContext.Provider value={[state, setState]}>
            { children }
        </MyContext.Provider>
    )
}
const useMyContext = () => useContext(MyContext);
export {MyProvider, useMyContext }

Layoutコンポーネント(各ページで共通のレイアウト)

export default function Layout({ children }) {
  return <div>{children}</div>;
}
//全てのページで共通のレイアウト
//_app.jsの<Component />を<Layout>でラップします
<Layout>
 <Component {...pageProps} />
</Layout>
import Head from "next/head";
import Script from 'next/script'
export default function Layout({ children, title = "デフォルトのタイトル" }) {
  return (
    <div>
      <Head>
        <title>{title}</title>
     <Script
          src="https://cdn..."
          strategy="beforeInteractive"
        ></Script>
      </Head>
      <header>
        <nav>
        </nav>
      </header>
      <main>
        {children}
      </main>
      <footer>
      </footer>
    </div>
  );
}
  • Headコンポーネント(next/head)は、head要素(title・metaなど)を追加するために使用します参考
  • Scriptコンポーネント(next/script)はサードパーティスクリプトは読み込みなどのために使用します(strategyは読み込みのタイミングです参考

ルーティング

「pagesフォルダ」の配下にJSファイルを作成するだけで、ファイルへのパスがURLパスになります
「index.js」はフォルダのルート

例:pages/about.js → /about
例:pages/index.js → /
例:pages/blog/index.js→/blog/

ページ名に角括弧「[]」を使うことで動的なルーティングを作成できます
フォルダパス:pages/[name]/settings.js
ファイルパス:pages/post/[pid].js

next/router
関数コンポーネントの中で「routerオブジェクト」 へアクセスしたいときは「 useRouterフック」を利用できます
pathname: 現在のルート( /pagesフォルダでのページのパス)
queryオブジェクト:オブジェクトに解釈されたクエリ文字列

import { useRouter } from 'next/router'

const Post = () => {
  const router = useRouter()
 //console.log(router)
  const { pid } = router.query
  return <p>Post: {pid}</p>
}

export default Post
ファイル名ルートqueryオブジェクト
pages/post/[pid].js/post/abc{ “pid”: “abc” }
/post/abc?foo=bar{ “foo”: “bar”, “pid”: “abc” }
pages/post/[pid]/[comment].js/post/abc/a-comment{ “pid”: “abc”, “comment”: “a-comment” }
pages/post/[…slug].js/post/a
/post/a/bなどにも一致
{ “slug”: [“a”] }
{ “slug”: [“a”, “b”] }
pages/post/[[…slug]].js/post
/post/a
/post/a/bなどにも一致
{ }
{ “slug”: [“a”] }
{ “slug”: [“a”, “b”] }

ページ遷移

next/router
useRouterを使用したページの移動(クライアント側)
router.push()

import { useRouter } from 'next/router'

export default function ReadMore() {
  const router = useRouter()

  return (
    <button onClick={() => router.push('/about')}>
      Click here to read more
    </button>
  )
}

next/link
Linkコンポーネント
aタグとの違い:Linkコンポーネントではページをロードしません

import Link from 'next/link';
function Home() {
  return (
    <ul>
      <li>
        <Link href="/">
          <a>Home</a>
        </Link>
      </li>
      <li>
        <Link href="/about">
          <a>About Us</a>
        </Link>
      </li>
    </ul>
  )
}
export default Home

レンダリングについて

3種類のレンダリング方法を使用でき、レンダリング方法はページごとに選択できます

  • サーバーサイドレンダリング(SSR
  • 静的サイトジェネレーター(SSG
  • クライアントサイドレンダリング(CSR
    *CSRはReactアプリケーションと同じ

クライアント側レンダリングとプレレンダリング

クライアント側レンダリング
ブラウザ側のJavaScriptでページをレンダリングします
CSR
プレレンダリング
Next.jsはデフォルトはプレレンダリングです
HTMLが事前に生成されます(クライアントに送信される前に外部データの取得やReactコンポーネンのHTML への変換が行われます)
SSR:HTML はリクエストごとに生成されます
SSG:HTMLはビルド時に生成されリクエストごとに再利用されます

SSGについて

HTMLはサーバー上で生成されますが、実行時にサーバーはありません(HTMLを返却するだけ)
ビルド時にHTMLが生成され、CDNに保存され、保存されたHTMLをリクエストごとに再利用します

ISR(Incremental Static Regeneration)
SSG動的なコンテンツの更新をする仕組みです
利用時の注意点:サーバの設定が複雑ですがデプロイ先が「Vercel」であれば簡単です

ビルド時にHTMLを生成
一定時間後にアクセスがあった場合、サー バー側はHTMLの更新と同時に「古い方のHTML」を返します
次のアクセスから更新した「新しいHTML」を返します
これを繰り返します

SSRはリクエストのたびにHTMLを生成するのでサーバー側で負荷が大きくなります
SSGはビルド済みのページを返すだけでCDNにキャッシュもでき高速なので、可能な限りSSGを使用します

外部データ取得

pages配下の関数コンポーネントはNode.jsとブラウザで実行されます

SSG

ビルド時にデータを取得してHTMLを事前に作成しておく
*頻繁に更新されるデータには適していません

next buildコマンド:本番用にアプリケーションをビルド「 .nextフォルダ」 を生成
next exportコマンド:静的なHTMLをエクスポート「 outフォルダ 」を生成(サーバー不要)

ビルドしてサーバーを起動

npm run build
npm start

外部データ取得して利用する場合

  • ページのコンテンツが外部データに依存する場合:getStaticProps
  • ページパスが外部データに依存する場合: getStaticPathsと通常はgetStaticProps両方を使用します

コンテンツを外部データから取得する
getStaticProps()は基本的にはビルド時に実行されます
*getStaticPaths()のfallbackによりパスをサーバーで生成しサーバーで実行される場合もあります
ビルド時に外部データをページのpropsに渡すことができます
*pages内でのみ利用可能です

//ビルド時にAPIから取得したデータ( posts)を受け取る
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}
// ビルド時に呼び出される関数
export async function getStaticProps() {
  // APIからデータを取得する
  const res = await fetch('https://.../posts')
  const posts = await res.json()
  return {
//ビルド時にpropsとして取得したデータ(posts)返す
    props: {posts},
  }
}

export default Blog

パスが外部データに依存する場合(動的ルートでページを静的に生成する)
getStaticPaths()
例:投稿の詳細画面(pages/posts/[id].js)
「getStaticPaths」をブログの投稿IDの一覧を取得し「getStaticProps」を使用してその投稿IDの記事データを取得します

function Post({ post }) {
  //省略
}
export async function getStaticPaths() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()
  // 事前にレンダリングしたいパスを取得します
//この例では投稿のid
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))
//fallback: falseは定義されていないパスからのアクセスは404ページを返却します
  return { paths, fallback: false }
}
//パスを視聴して投稿データを取得、そのページを事前にレンダリング
export async function getStaticProps({ params }) {
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()
  return { props: { post } }
}
export default Post

fallbackについて
fallbackは事前ビルドしたパス以外にアクセスがあったときの動作を決めます
(getStaticPathsから返されたパス)

  • falseではビルド時に生成されなかったパスは404になります
  • trueではビルド時に生成されなかったパスは404になりません
    パスはSSRを実行し生成されたHTMLを返します

fallbackがtrueの場合
router.isFallbackがtrueならファールバック中になります
*fallbackが”blocking”の場合はローディングは表示されません(ファールバックはスキップされます)

import { useRouter } from "next/router";

export default function Post({ post }) {
  const router = useRouter();
  if (router.isFallback || !post) {
    return <div>Loading...</div>;
  }

}

ISR(Incremental Static Regeneration)

ISRは、リクエストに対して静的生成されたページを返し、有効期限を超えたら静的ページを再生成します
ある程度の頻度でアクセルがあるサイトであれば、SSGでコンテンツの「更新」に対応できます

ISRを使用するには、getStaticProps()に「revalidate(有効期限)」を追加します

function Blog({ posts }) {
 //省略
}
export async function getStaticProps() {
  const res = await fetch('https://...')
  const posts = await res.json()
  return {
    props: {
      posts,
    },
    // 60秒に1回
    revalidate: 60, 
  }
}

export async function getStaticPaths() {
  const res = await fetch('https://...')
  const posts = await res.json()
  //パスを取得
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))
  return { paths, fallback: 'blocking' }
}
export default Blog

revalidate: 60を設定した場合

  • 60秒間は静的な HTMLのキャッシュを返し
  • 60秒経過した後にリクエストがあったタイミングでデータの再取得してページを再生成をします(このリクエストに対しては古いHTMLのキャッシュを返します)
  • 次のリクエストに対しては再生成した新しいHTMLのキャッシュを返します
  • この動作を繰り返します

SSR

リクエストがあるたびにHTML生成
頻繁に更新されるデータをページで事前にレンダリングする必要がある(SEOを考慮する)場合

getServerSideProps()
Node.jsで実行される関数です
getServerSidePropsはリクエスト時に呼び出されサーバーで実行されます
*pages内でのみ利用可能です

function Page({ data }) {
  //省略
}

// すべてのリクエストで呼び出されます
export async function getServerSideProps() {
  const res = await fetch(`https://.../data`)
  const data = await res.json()
  return { props: { data } }
}

export default Page

引数(context)にはリクエストやレスポンスの情報が含まれます

//パスを取得
export async function getServerSideProps(context) {
 return { 
   props : { context.query } 
}

CSR(クライアントサイド)

ページが読み込まれたら、JavaScript を使用してクライアントから外部データを取得し、外部データ部分を表示します(外部データを必要としないページの部分を静的生成します)

「useEffect」を使用してクライアント側でデータを取得
「useEffect」内に書かれた関数はブラウザで実行されます

 const [data, setData] = useState(null)
  const [isLoading, setLoading] = useState(false)

  useEffect(() => {
    setLoading(true)
    fetch('/api/profile-data')
      .then((res) => res.json())
      .then((data) => {
        setData(data)
        setLoading(false)
      })
  }, [])
  if (isLoading) return <p>Loading...</p>
  if (!data) return <p>No profile data</p>
  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.bio}</p>
    </div>
  )
}

useSWR(データ取得用のReactフック)
クライアント側でデータをフェッチする場合、推奨されています

import useSWR from 'swr';

function Profile() {
  const { data, error } = useSWR('/api/user', fetch);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

APIルート

pages/apiフォルダ
アプリ内にAPI エンドポイントを作成できます
サーバーレス関数 (Lambda とも呼ばれます) としてデプロイもできます

/api/helloでアクセス
{“text”:”Hello”}が表示されます

export default function handler(req, res) {
   res.status(200).json({ text: 'Hello' });
}

Netlifyに静的ファイルをデプロイする手順

  • GitHubリポジトリの作成: Next.jsプロジェクトのコードが含まれるGitHubリポジトリを作成
  • Netlifyダッシュボードにログイン後、「New site from Git」ボタンをクリック
  • GitHubと連携: 「Continuous Deployment」セクションでGitHubを選択し、リポジトリを選ぶ
  • ビルド設定
    Build command: npm run build && next export
    Publish directory: out
  • 環境変数の設定が必要で
    Site settings” > “Build & Deploy” > “Environment”でNETLIFY_NEXT_PLUGIN_SKIP = trueを設
  • 画像が表示されない場合: next.config.jsでimages: { unoptimized: true }を設定