Next.jsの基礎

ここでは static async getInitialProps() やルーティング、スタイリングなどNext.jsの中で重要な要素について紹介していきたいと思います。この記事のNext.jsの対象バージョンは 7.0.2 です。全部を説明することはできませんので詳細についてはその都度公式ドキュメント (Dynamic Importやビルド設定の編集、環境変数の設定など色々な機能があります) を参照してください。

目次

Server Side Renderingでのデータ取得

通常のReactアプリで外部のAPIサーバーからデータを持ってくる場合には基本的にはAjaxを使って、データを取得してコンポーネント内部で表示します。Next.js ではServer Side Renderingをするので サーバーサイドでも 、クライアントサイドでもデータを外部APIサーバーなどから取得する必要があります。そのため、 isomorphic-unfetch などの両方の環境で動くライブラリを通常は使います。

isomorphic-unfetch

Next.js では上記の外部サーバーからのデータ取得を static async getInitialProps() メソッド内で定義します。そして、このメソッドはReactコンポーネント内部に下記のように定義します。

import fetch from 'isomorphic-unfetch'

export default class SomePage extends React.Component {
  /* 
    static async getInitialProps()は下記のタイミングで呼ばれる。
    基本的には、サーバーサイド・クライアントサイド、どちらの環境でも動作するコードにする必要がある。

    1: サーバーサイドレンダリングでReactコンポーネントをレンダリングする時
    2: Next.jsのルーター経由でこのページにアクセスした時
  
  */
  static async getInitialProps({ Component, router, ctx }) {
    try {
      // 外部APIサーバーからデータを取得。
      const res = await fetch('https://!?#$%&@{[]}.com/posts')
      
      const myBlogPosts = await res.json()
      // 例: [{"title": "Next.jsでアプリをつくってみた"}, {"title": "workbox-swをためす"}]
      
      // ここで return したデータがPropsとしてコンポーネントに渡されてくる。
      return { posts: myBlogPosts }
    } catch(e) {
      console.log(e)

      // エラーが発生した時に返すデータ。
      return {"posts": []}
    }
  }

  /* getInitialProps()で返した値がコンポーネントにPropsとして渡されてくる */
  // サーバーサイドおよび、クライアントサイドで取得したデータをもとにレンダリング。
  render () {
    return (
      <ul>
        {this.props.posts.map((post) => <li>{post.title}</li>)}
      </ul>
    )
  }
}

上記の static async getInitialProps() メソッドは下記のタイミングで呼ばれます。

  • ※ サーバーサイドでReactコンポーネントがレンダリングされる時
  • ※ Next.jsのルーティング (=クライアントサイドルーティング) 経由でページにアクセスされた時

該当ページを表示するために必要な初期データをこのメソッド内部で取得します。この関数で返したオブジェクトがPropsとしてReactコンポーネントに渡されてきます。

CSSの定義

Next.js開発元のZeit社によって手軽にサーバーサイドレンダリング込みの CSS-in-JS を実現するライブラリとしてstyled-jsxが提供されています。styled-jsx は比較的簡単にNext.jsに組み込むことができ、簡単にCSSのスコープ化を実現することができます。

export default () => (
  <div>
    <h1>Blog!</h1>
    <style jsx>{`
      p {
        color: gray;
      }
      strong {
        color:red;
      }
      @media (max-width: 600px) {
        div {
          background: blue;
        }
      }
    `}</style>
    <div>
        <p>春は<strong>あけぼの</strong>。やうやう白くなりゆく山際、少し明かりて、紫だちたる雲の細くたなびきたる。 </p>
        <p>
        夏は<strong>夜</strong>。月のころはさらなり、闇もなほ、蛍の多く飛びちがひたる。また、ただ一つ二つなど、ほのかにうち光て行くもをかし。雨など降るもをかし。 </p>
    </div>
  </div>
)

もし、styled-jsxではなく、styled-componentsをCSS-in-JSのライブラリとして使いたい場合には、styled-componentsのサーバーサイドレンダリング処理を後述する _document.js 等のNext.jsの共有レイアウトで施すことで対応することができます。

/* pages/_document.tsx */
// styled-componentsのサーバーサイドレンダリング (一例)

import React from "react";
import Document, { Head, Main, NextScript } from "next/document";

export default class MyDocument extends Document {
  static async getInitialProps(ctx: any) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;
    ctx.renderPage = () =>
      originalRenderPage({
        enhanceApp: (App: any) => (props: any) =>
          sheet.collectStyles(<App {...props} />)
      });

    const initialProps = await Document.getInitialProps(ctx);
    return {
      ...initialProps,
      styles: [...(initialProps.styles as any), ...sheet.getStyleElement()]
    };
  }

  render() {
    return (
      <html>
        <body>
          ...
        </body>
      </html>
    );
  }
}

CSS ( Next.js ドキュメント) 

共通レイアウト・共通処理の定義

_document.js(.tsx) をカスタマイズする

Next.jsのPageコンポーネントはデフォルトで<html>や<body >タグの定義を行いますが、デフォルトの共通レイアウトをカスタマイズしたい場合には、「pages/_document.js」 というファイルを定義して、その中で共通レイアウトを定義します。この「pages/_document.js」 はサーバーサイド側のみで呼び出され、クライアント側では呼ばれません。先述した styled-components のサーバーサイドレンダリング処理などがこの中で記述できます。

_document.js(.tsx)のカスタマイズ (Next.js ドキュメント)

// _document.jsはサーバーサイドレンダリング時のみ呼び出される。
// そのため、onClickなどのイベントハンドラを定義することはできない。

import Document, { Head, Main, NextScript } from 'next/document'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx)
    return { ...initialProps }
  }

  render() {
    return (
      <html>
        <Head>
          <meta name="theme-color" content="#ff0000">
          <meta name="format-detection" content="telephone=no">
          <meta name="viewport" content="width=device-width,initial-scale=1">
          <style>{`html,body { margin: 0; padding: 0; }`}</style>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    )
  }
}

個々のページでの<head> タグのカスタマイズ

<head> タグは各ページでオーバーライドすることができます。共通のタグは _document.js で定義して、個別のページでtitleやその他のmetaタグをカスタマイズするといったことができます。

import Head from 'next/head'

export default () => (
  <div>
    <Head>
      <title>My blog</title>
      <meta name="description" content="this is myblog" />
    </Head>
    <p>Hello my blog</p>
  </div>
)

<head> タグの編集 (Next.js ドキュメント)

_app.jsをカスタマイズする

Next.jsでは各ページが初期化されるときに App というNext.js内部に組み込まれたコンポーネントが初期化される仕組みになっています。この App コンポーネントはカスタマイズすることができ、「pages/app.js」というファイルを作成することで、ページ間で共通のレイアウトをもたせたり、共通のStateをもたせたりすることができます。こちらは、「pages/document.js」と異なり、クライアントサイドでも実行されます。

import React from 'react'
import App, { Container } from 'next/app'

export default class MyApp extends App {
  render () {
    return (
      <Container>
        <nav>
            <ul>
                 <li><Link href="/"><span>Top</span></Link></li>                
                 <li><Link href="/about"><span>About</span></Link></li>
                 <li><Link href="/blog"><span>Blog</span></Link></li>
            </ul>
        </nav>        
        <Component />
     </Container>
    )
  }
}

_app.jsのカスタマイズ (Next.js ドキュメント)

ルーティング

Next.jsにはReact Routerのようなクライアントサイドでのルーター機能が組み込まれています。"next/Link"からLinkをimportして用いることで各ページにNext.jsのルーターを経由して、アクセスすることができます。

import Link from 'next/link'

export default () => (
  <nav>
    <ul>
     <li><Link href="/about"><span>About</span></Link></li>
     <li><Link href="/blog"><span>Blog</span></Link></li>
     <li><Link href="/contact"><span>Contact</span></Link></li>
     <li><Link href="/secret"><span>Secret</span></Link></li>
    </ul>
  </nav>
)

サーバー・クライアントの書き分け

Next.jsはユニバーサルなフレームワークです。基本的には、JavaScriptのコードはサーバー側でもクライアントサイド側でも共通して動くようにしなければなりません。さもなければアプリケーションの処理はクラッシュします。

しかし、ライブラリの特性上、どうしてもブラウザ環境特有のwindowやdocumentにアクセスしなければならない場合、(例えばブログのシンタックスハイライトなどのライブラリを適用する場合)、もしくはFIleSystemなどNode.js特有のモジュールを使いたい場合にはどうすればよいでしょうか? 普通にそういうライブラリを import してしまうとサーバー側とクライアントサイド側の片方の処理が動かないため、アプリケーションはクラッシュしてしまいます。

Next.js は抜け道として公式にいくつか手段を提案しています。例えば、クライアントサイドだけで動作したいコードを記述したい場合には、Reactでおなじみの componentDidMount() 内部に処理を記述します。componentDIdMount() はサーバーサイドレンダリング時には呼ばれず、クライアントサイドでのみ呼ばれます。また、windowオブジェクトがundefinedであることを typeof演算子 を使って確認した上で、処理を実行する方法もあります。

windowを参照するライブラリを読み込む (FAQ)

import React from 'react'

if (typeof window !== 'undefined') { 
    // クライアントサイドのみで実行したい処理を実装
    require('some-client-side-library-x');
}

class MyBlogComponent extends React.Component {
  componentDidMount() {
    // クライアントサイドのみで実行したい処理を実装
    const mySyntaxHighlighter = require('my-syntax-highlighter-xyzxyzxyz');
    mySyntaxHighlighter.exec();
  }

  render() {
    return (
       <div>
            ...
       </div>
    )
  }
}

一通り、重要な要素について紹介をした上で次はNext.jsを使って実装を開始していきたいと思います。