SPA
React
next.js
ssr
Next.jsDay 2

Next.jsチュートリアル翻訳

Next.jsでは公式が提供するチュートリアルがあります。
GitHubアカウントがあれば始められて、ところどころクイズ形式になっていて、正解するとpointが貯まるちょっと楽しい仕様になっています。
ただ、英語が苦手な方や、ざっと全容を把握したい、という方もいらっしゃると思うので、チュートリアルを自分なりにまとめてみます。
Vue.jsやNuxt.jsが日本で普及しているのは翻訳を提供しているのが大きいと思う(コミュニティとして巻き込んでいくのももちろん大きいと思う)ので、微力ながらNext.jsの普及に貢献したいと思って記事を書きます。
意訳や情報の歯抜けがあるかとは思いますが、ご指摘もしくはご容赦下さい。


この記事はNext.jsチュートリアル https://nextjs.org/learn/ の翻訳です。

はじめてみる

環境構築

Windows, MacやLinuxでも、Node.jsがインストールされていればNext.jsを始められます。
まずはじめに、ターミナルを使って下記コマンドを実行してみましょう。

shell
mkdir hello-next
cd hello-next
npm init -y
npm install --save react react-dom next
mkdir pages

次にhello-nextディレクトリにあるpackage.jsonを開き、下記NPMスクリプトを追記します。

package.json
{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }
}

次に下記をターミナルで実行すればhttp://localhost:3000でアプリケーションが起動します。

shell
npm run dev

では試しにhttp://localhost:3000にアクセスしてみましょう。ここで何が起こるかと言うと、Next.jsがデフォルトで用意しているエラーページがレンダリングされます。

404page

今回は404の例でしたが、500系のエラーでも同様にデフォルトのエラーページページが出てきます。
カスタマイズしたエラーページを作成したい場合はpages/_error.jsというファイル名で作成できます。詳しくはこちら

はじめてのページを作る

Next.jsはpagesディレクトリ配下のファイルがそのままルーティングとなってくれます。
pages/index.jsに最初のページを作ってみましょう。

pages/index.js
const Index = () => (
  <div>
    <p>Hello Next.js</p>
  </div>
)

export default Index

http://localhost:3000に再度アクセスしてみるとHello Next.jsとレンダリングされているはずです。
pagesディレクトリ配下のファイルで、Reactコンポーネントをdefault exportさせることで、ページのコンポーネントとして認識されます。

では <p>Hello Next.jsというように閉じタグ忘れをした場合どうなるか。
Next.jsがエラートラッキングをブラウザに表示してくれます。
正しく修正がなされるとHMRによって自動でページが再レンダリングされます。

ページ間移動をする

先程作った"Index"ページの他に"About"ページを作ってみましょう。
pagesディレクトリ配下に下記内容でpages/about.jsを作ります。

pages/about.js
export default () => (
  <div>
    <p>This is the about page</p>
  </div>
)

すると、ファイル名に紐付いたURLのhttp://localhost:3000/aboutにアクセスできるようになりました。
次に考えるのは"a"タグでページ間を移動できるようにすることです。
しかし、Next.jsでは"a"タグのみでリンクを作ると、毎回サーバに問い合わせが行ってしまいます。
SPAの挙動である、クライアントサイドのみでページ遷移を行うためにはどうしたらよいでしょうか。
答えはNext.jsが提供するリンクAPIのnext/linkを使うことです。

pages/index.jsにリンクAPIを追加して"About"ページと繋いでみましょう。

pages/index.js
// これがリンクAPIです
import Link from 'next/link'

const Index = () => (
  <div>
    {/* aタグを<Link>コンポーネントで囲い、hrefなどのリンクの情報は<Link>に渡します。 */}
    <Link href="/about">
      <a>About Page</a>
    </Link>
    <p>Hello Next.js</p>
  </div>
)

export default Index

http://localhost:3000にアクセスし、"About Page"リンクを押すとページ遷移できるようになりました。
この遷移はクライアントサイドつまりブラウザでのみ起こり、サーバーサイドにはリクエストを発しません。
確かめるにはブラウザの開発者ツールでネットワークを見てみましょう。

また、ブラウザの戻るボタンを押すと、クライアントサイドのみで戻ります。
これはつまり、next/linklocation.historyをすべてハンドリングしてくれているわけです。

リンクをスタイリングする

リンクをスタイリングするには"a"タグにpropsを渡します。

<Link href="/about">
  <a style={{ fontSize: 20 }}>About Page</a>
</Link>

これはnext/linkHOCとなっていて、"href"など必要なpropsのみを受取るようになっているからです。

buttonタグでリンクを作る

aタグの代わりにbuttonタグでリンクも作れます。

<Link href="/about">
  <button>Go to About Page</button>
</Link>

Reactコンポーネントなら"div"タグでも何でもOKです。
ただし、直下は単一のコンポーネントを返さないといけないのと、onClickpropを受け取れるコンポーネントでなくてはいけません。
next/linkはprefetch機能などもあり、とてもパワフルです。詳しくはこちら。

共通のコンポーネントを使う

ここまでで、pagesディレクトリ配下にReactコンポーネントをexportして置くと、ファイル名に紐付いたURLによる固定ページが出来ることを学びました。
exportされたページはJavaScriptのモジュールなので、他のコンポーネントをimportできます。ここのセクションは普通のReactの挙動の話です。

Headerコンポーネントを作る

共通のコンポーネントとなるHeaderコンポーネントを作っていきましょう。
componentsディレクトリをルート直下に掘り、下記内容でファイルを作ります。

components/Header.js
import Link from 'next/link'

const linkStyle = {
  marginRight: 15
}

const Header = () => (
    <div>
        <Link href="/">
          <a style={linkStyle}>Home</a>
        </Link>
        <Link href="/about">
          <a style={linkStyle}>About</a>
        </Link>
    </div>
)

export default Header

2つのリンクは見やすくするためにスタイリングしています。

Headerコンポーネントを使用する

ここまでで作ったページにHeaderコンポーネントをimportして既存のリンクを上書きしてみましょう。

pages/index.js
import Header from '../components/Header'

export default () => (
  <div>
    <Header />
    <p>Hello Next.js</p>
  </div>
)

そしてpages/about.jsでも同様にHeaderコンポーネントimportして下さい。

page navigation

ところで、componentsというディレクトリ名をcompsに変更し、ファイル内も置換し、サーバを再起動すると問題なく動きます。
コンポーネントを置くディレクトリ名は何でもよく、Next.jsにおける特別なディレクトリはpagesだけになります。

レイアウトコンポーネントを作る

現実世界ではHeaderの他にFooterなども作るはずで、逐一pagesのコンポーネントに追加するのは面倒です。
そこで共通となるLayoutコンポーネントを、下記内容でcomponents/MyLayout.jsに作りましょう。(前のセクションでcompsディレクトリを作った人はもとのcomponentsに戻してください。)

components/MyLayout.js
import Header from './Header'

const layoutStyle = {
  margin: 20,
  padding: 20,
  border: '1px solid #DDD'
}

const Layout = (props) => (
  <div style={layoutStyle}>
    <Header />
    {props.children}
  </div>
)

export default Layout

MyLayoutが出来たら、各ページで使えるようになります。

pages/index.js
import Layout from '../components/MyLayout.js'

export default () => (
    <Layout>
       <p>Hello Next.js</p>
    </Layout>
)
pages/about.js
import Layout from '../components/MyLayout.js'

export default () => (
    <Layout>
       <p>This is the about page</p>
    </Layout>
)

ところで、MyLayout.jsから{props.children}を取り除くとどうなるでしょうか。
各ページのコンテンツが表示されなくなります。
これはラップされたコンポーネントは親コンポーネントのprops.childrenに割り当てられるため、それをレンダリングしなくなるからです。

また、Layoutコンポーネントの表現方法はこれだけではなく、他の方法でも作れます。

Layout例1
import withLayout from '../lib/layout'

const Page = () => (
  <p>This is the about page</p>
)

export default withLayout(Page)
Layout例2
const Page = () => (
  <p>This is the about page</p>
)

export default () => (<Layout page={Page}/>)
Layout例3
const content = (<p>This is the about page</p>)

export default () => (<Layout content={content}/>)

ここまでで以下2点のユースケースに触れました。

  1. 共通のHeaderコンポーネント
  2. レイアウトコンポーネント

コンポーネントはスタイリングやページレイアウトなど、好きなタスクに使うことができます。
加えて、NPMモジュールからもコンポーネントをimportして使うこともできます。

動的ページを作成する

ここまでで複数ページのNext.jsアプリケーションをどのように作成するかを学びました。ページを作成するためには、実際にファイルを作成するのです。

しかし、現実世界では動的コンテンツを生成するために動的なページを作らなくてはいけません。Next.jsではそれに対して様々なアプローチを用意しています。

まずはクエリ文字列(query strings)による動的ページから始めましょう。
ホームページ(index)にすべての投稿リストがあるような、シンプルなブログを作っていきます。

環境構築

今まで使ってきたhello-nextは置いておいて、また別にExampleをダウンロードしましょう。

shell
git clone https://github.com/arunoda/learnnextjs-demo.git
cd learnnextjs-demo
git checkout using-shared-components

こちらのコマンドで実行できます:

shell
npm install
npm run dev

投稿リストを追加する

まず最初に、ホームページに投稿のタイトルリストを作成しましょう。
以下の内容をpages/index.jsに加えます。

pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'

const PostLink = (props) => (
  <li>
    <Link href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
)

export default () => (
  <Layout>
    <h1>My Blog</h1>
    <ul>
      <PostLink title="Hello Next.js"/>
      <PostLink title="Learn Next.js is awesome"/>
      <PostLink title="Deploy apps with Zeit"/>
    </ul>
  </Layout>
)

完了すると、このようなページが出来上がります:
posts

この状態で一番上のリンクを踏むと404ページに行きます。
するとURLは/post?title=Hello%20Next.jsという形になっているはずです。

クエリ文字列を経由してデータを受け渡す

ここではクエリ文字列(クエリパラメータ)を経由してデータを受け渡しています。
以下のPostLinkコンポーネントにおいて、"title"をクエリパラメータとして使用しています。

const PostLink = (props) => (
  <li>
    <Link href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
)

(Linkコンポーネントのhrefをチェックしてみて下さい。)
このようにして、どんなデータでもクエリ文字列として渡す事ができます。

投稿の詳細ページを作成する

次に、ブログの投稿を表示するための投稿詳細ページを作る必要が出てきました。そのためにはクエリ文字列からタイトルを取得しなくてはいけません。どのように操作するか見ていきましょう。

下記の内容でpages/post.jsファイルを作成します:

pages/post.js
import {withRouter} from 'next/router'
import Layout from '../components/MyLayout.js'

const Page = withRouter((props) => (
    <Layout>
       <h1>{props.router.query.title}</h1>
       <p>This is the blog post content.</p>
    </Layout>
))

export default Page

するとhttp://localhost:3000/post?title=Hello%20Next.jsでは:

post

といった形で表示されます。

上記コードの中で何が起こっているというと、

  • "withRouter"関数を"next/router"からimportし使用しました。この関数はNext.jsのルーターをプロパティとして注入するものです。
  • この場合、クエリ文字列である、ルーターの"query"オブジェクトを使用しています。
  • それによって、投稿タイトルがprops.router.query.titleとして取得することができました。

アプリケーションの簡単なブラッシュアップをしていきましょう。以下の内容でpages/posts.jsを書き換えます。

pages/posts.js
import {withRouter} from 'next/router'
import Layout from '../components/MyLayout.js'

const Content = (props) => (
  <div>
    <h1>{props.router.query.title}</h1>
    <p>This is the blog post content.</p>
  </div>
)

const Page = withRouter((props) => (
    <Layout>
       <Content />
    </Layout>
))

export default Page

ここでhttp://localhost:3000/post?title=Hello%20Next.jsにアクセスすると何が起こるでしょうか。

post error

ご覧のとおりエラーが発生します。
これはなぜかというと、withRouterを呼び出して得られるrouterプロパティをPageコンポーネントに対して注入してしてしまっているからです。
withRouterはNext.jsアプリケーションのどのコンポーネントでも使用することができます。なので、この場合はwithRouterの呼び出しをContentコンポーネントに移動させましょう:

pages/posts.js
import {withRouter} from 'next/router'
import Layout from '../components/MyLayout.js'

const Content = withRouter((props) => (
  <div>
    <h1>{props.router.query.title}</h1>
    <p>This is the blog post content.</p>
  </div>
))

const Page = (props) => (
    <Layout>
       <Content />
    </Layout>
)

export default Page

ここまでで、動的ページをクエリ文字列を使用して作成する方法を学びました。これはまだまだ始まりに過ぎません。

動的なページはレンダリングするのにもっと情報が必要なはずです。また、クエリ文字列を経由してすべてのデータを渡すことは出来ないかもしれません。そしてhttp://localhost:3000/blog/hello-nextjsのような明確なURLを持ちたくなるかもしれません。
次のレッスンではそれらについて学んでいきます。

ルートマスキングをしたクリーンURL

直前のレッスンでは、クエリ文字列を用いてどのように動的ページを作成するかを学びました。
このときのURLはこのような形でした:
http://localhost:3000/post?title=Hello%20Next.js

しかし、あまり綺麗ではありませんね。
ではこのようなURLだったらどうでしょうか:
http://localhost:3000/p/hello-nextjs

こっちのほうが良いですよね。このレッスンではこのようなURLで動的ページを生成していきます。

環境構築

create-dynamic-pagesブランチをチェックアウトしましょう。サーバの再起動まで行います。

shell
git clone https://github.com/arunoda/learnnextjs-demo.git
cd learnnextjs-demo
git checkout create-dynamic-pages
npm install
npm run dev

ルートマスキング

ルートマスキングというNext.jsのユニークな機能を使っていきましょう。要するに、あなたのアプリケーションが実際に参照するURLとは違ったURLがブラウザに表示されるようになります。

ブログ投稿詳細URLにルートマスキングを施しましょう。

以下のコードをpages/index.jsで使用します。

pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'

const PostLink = (props) => (
  <li>
    <Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
)

export default () => (
  <Layout>
    <h1>My Blog</h1>
    <ul>
      <PostLink id="hello-nextjs" title="Hello Next.js"/>
      <PostLink id="learn-nextjs" title="Learn Next.js is awesome"/>
      <PostLink id="deploy-nextjs" title="Deploy apps with Zeit"/>
    </ul>
  </Layout>
)

以下のコードのかたまりを見てみましょう:

const PostLink = (props) => (
  <li>
    <Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
)

<Link>要素では、"as"というpropを使用しました。これがブラウザで表示する必要があるURLになります。アプリケーションが参照するのは"href"というpropの中身になります。


さて、最初の投稿のリンクをクリックすると、投稿の詳細が表示されるはずです。
では、ブラウザの戻るボタンを押し、さらに進むボタンを押してみるとどうなるでしょう。
答えはindexページに戻って、また投稿の詳細ページに遷移します。
あなたが目撃したとおり、ルートマスキングはブラウザ履歴とともにいい感じに動いてくれます。リンクに"as"propを足せばよいだけなのです。

再読込

http://localhost:3000に戻りましょう。最初の投稿のタイトルをクリックすると、投稿詳細ページに遷移します。
そこでブラウザの再読込を走らせて下さい。何が起こるでしょうか?
Next.jsは404エラーを出力します。
なぜかというと、サーバで読み込む用の、そのようなページは存在していないからです。
サーバはp/hello-nextjsを読み込もうとします。しかし今の所2つのページしか用意されていません:index.jspost.jsです。
これではプロダクションで実行できないので、直さなくてはいけませんね。
Next.jsのカスタムサーバAPIがこの問題を解決してくれます。
次のレッスンではこれを学んでいきましょう。

クリーンURLのためのサーバーサイドでのサポート

直前のレッスンではクリーンURLの作り方を学びました。要するにこんな感じのURLを持てるようになります。
http://localhost:3000/p/my-blog-post

しかし、これはクライアントでの遷移のみで有効でした。ページをリロードすると、404ページが現れます。
なぜかというと、p/my-blog-postというページはpagesディレクトリ配下に実際には存在しないからです。
そこで、Next.jsのカスタムサーバAPIを使えばとても簡単に解決できます。

環境構築

変更をリセットして、別ブランチでサーバの再起動までしましょう。

shell
git clone https://github.com/arunoda/learnnextjs-demo.git
cd learnnextjs-demo
git checkout using-shared-components
npm install
npm run dev

カスタムサーバを作る

Expressを使ってカスタムサーバを作っていきます。とてもシンプルです。

まず最初にExpressをアプリケーションにインストールします。

shell
npm install --save express

次にserver.jsというファイルをルート直下に作成し、以下の内容に書き換えてください:

server.js
const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
.then(() => {
  const server = express()

  server.get('*', (req, res) => {
    return handle(req, res)
  })

  server.listen(3000, (err) => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
})
.catch((ex) => {
  console.error(ex.stack)
  process.exit(1)
})

そしてpackage.jsonの"scripts"の内容を下記に書き換えます:

package.json
{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  }
}

書き換えたらもう一度npm run devでサーバを再起動してみましょう。すると何が起こりましたか?

アプリケーションは動いたが、サーバーサイドでのクリーンURLは動かない、という状態だと思います。

カスタムルートを作成する

先程のレッスンで分かったと思いますが、以前のようにアプリケーションは動きます。なぜかというと、さきほど書いたカスタムサーバはNext.jsがデフォルトで用意しているバイナリコマンドに似ていたからです。

それでは、投稿の詳細ページのURLにマッチするように、カスタムルートを追加していきましょう。

新しいルートを加えると、server.jsはこのようになります:

server.js
const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
.then(() => {
  const server = express()

  server.get('/p/:id', (req, res) => {
    const actualPage = '/post'
    const queryParams = { title: req.params.id } 
    app.render(req, res, actualPage, queryParams)
  })

  server.get('*', (req, res) => {
    return handle(req, res)
  })

  server.listen(3000, (err) => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
})
.catch((ex) => {
  console.error(ex.stack)
  process.exit(1)
})

以下のコードに注目して下さい:

server.get('/p/:id', (req, res) => {
    const actualPage = '/post'
    const queryParams = { title: req.params.id } 
    app.render(req, res, actualPage, queryParams)
})

ここでは、カスタムルートを"/post"という既存ページにマップさせています。そしてクエリ文字列も同様にマップさせています。
そう、それだけです。
ではアプリケーションを再起動して、以下のページにアクセスしてみましょう。そして再読込をしましょう。
http://localhost:3000/p/hello-nextjs

もう404ページは現れませんね。
しかし少しだけ問題が発生しました。何か分かるでしょうか?

答えは、クライアントサイドのタイトルと、サーバーサイドレンダリングされたタイトルが異なっていることです。

URLの情報

/postページはクエリ文字列パラメータのtitleを受け取ります。クライアントサイドでのルーティングでは、URLマスキングされた正確な値を、簡単に受け渡すことができます。(つまりLinkコンポーネントの"as"propによってです。)

<Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}>
  <a>{props.title}</a>
</Link>

しかし、サーバーサイドのルートではタイトルを持っていません。なぜならURL中の投稿ID(:id)のみしか情報が得られないからです。投稿IDをサーバーサイドのクエリ文字列パラメータとして設定していました。

以下のルート定義を見てみると:

server.get('/p/:id', (req, res) => {
  const actualPage = '/post'
  const queryParams = { title: req.params.id } 
  app.render(req, res, actualPage, queryParams)
})

これでは問題があります。しかし、クライアントサイドとサーバーサイド両方でデータ取得をするのにはIDを使うはずなので現実世界ではそこまで問題になりません。
なので、IDのみが必要な情報です。

このレッスンではNext.jsカスタムサーバAPIを使用して簡単にルートを作ることが出来ました。それに加え、サーバーサイドでのクリーンURLも追加しました。このように、ほしいルートを好きなだけ作ることが出来ます。

また、Expressだけでなく、Node.jsサーバフレームワークならどれでも使用可能です。詳しくはカスタムサーバAPIのドキュメントを見て下さい。

ページのためのデータfetchをする

ここまででまずまずのNex.jsアプリケーションを作ることができましたし、Next.jsルーティングAPIの利点を最大限に得ることができました。

実践ではたいてい、リモートのデータソースからデータをfetchしなくてはいけません。Next.jsでは、ページ用にデータfetchをする標準APIを搭載しています。それは非同期関数のgetInitialPropsと呼ばれるものです。

これによって、リモートのデータソースからのデータfetchをページのために行えるだけでなく、propsとしてページにデータを渡すことも出来ます。サーバーサイドとクライアントサイド両方で動くgetInitialPropsをNext.jsは用意しているのです。

このレッスンではgetInitialPropsTVmaze APIを使って、バットマンTVショーの情報を表示するアプリを作っていきます。

環境構築

変更をリセットして、別ブランチでサーバの再起動までしましょう。

shell
git clone https://github.com/arunoda/learnnextjs-demo.git
cd learnnextjs-demo
git checkout clean-urls-ssr
npm install
npm run dev

バットマンのデータをfetchする

今まで作ってきたデモアプリケーションはブログの投稿をホームページにリストで表示させるものでした。それではバットマンの番組のリストを表示させましょう。
ハードコーディングするのではなく、リモートのサーバからデータをfetchしてきましょう。
ここではテレビ番組の情報を検索できるTVmaze APIを使っていきます。

まず最初にisomorphic-unfetchをインストールする必要があります。これはデータfetchをするためのライブラリで、ブラウザのfetch APIをでシンプルに実行できるだけでなく、クライアントとサーバーサイドの両方で動いてくれます。

shell
npm install --save isomorphic-unfetch

では、pages/index.jsを以下の内容に書き換えましょう:

pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'
import fetch from 'isomorphic-unfetch'

const Index = (props) => (
  <Layout>
    <h1>Batman TV Shows</h1>
    <ul>
      {props.shows.map(({show}) => (
        <li key={show.id}>
          <Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
            <a>{show.name}</a>
          </Link>
        </li>
      ))}
    </ul>
  </Layout>
)

Index.getInitialProps = async function() {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman')
  const data = await res.json()

  console.log(`Show data fetched. Count: ${data.length}`)

  return {
    shows: data
  }
}

export default Index

ここまで触れてきたIndex.getInitialPropsという関数がちゃんと入っていますね:

Index.getInitialProps = async function() {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman')
  const data = await res.json()

  console.log(`Show data fetched. Count: ${data.length}`)

  return {
    shows: data
  }
}

getInitialPropsはstatic async関数で、アプリケーションのどのページにも追加できます。これを使うことでfetchしたデータをページにpropsとして渡すことができます。

http://localhost:3000にアクセスすると、見ての通り、バットマンの番組情報をfetchして、ページに"shows" propとして受け渡すことが出来ました。
batman


getInitialProps関数を見ると、fetchしたデータの数をコンソール出力するコードが書いてあります。
ブラウザのコンソールと、サーバのコンソール両方を見てみましょう。
ではページを再読込みします。
ページがロードされたあと、どこに出力が見えたでしょうか?

正解はサーバのコンソールでした。

ページのリロードの場合だと、サーバのみに表示されます。
なぜかと言うと、ページはサーバでレンダリングされるからです。
データはサーバ側で既に得ていたので、クライアントで再びfetchする理由は無いわけです。

投稿詳細ページでの実行

では、テレビ番組の詳細な情報が表示される"/post"ページで実行していきましょう。

最初に、server.jsを開いて、/p/:idのルートを以下の内容に変更します:

server.js
server.get('/p/:id', (req, res) => {
    const actualPage = '/post'
    const queryParams = { id: req.params.id }
    app.render(req, res, actualPage, queryParams)
})

それが終わったら変更を適用させるためにサーバを再起動して下さい。
先程はtitleのクエリパラメータをページにマップさせていました。今度はそれをidにリネームさせる必要があります。

そして、pages/post.jsを以下の内容で書き換えてください:

pages/post.js
import Layout from '../components/MyLayout.js'
import fetch from 'isomorphic-unfetch'

const Post =  (props) => (
    <Layout>
       <h1>{props.show.name}</h1>
       <p>{props.show.summary.replace(/<[/]?p>/g, '')}</p>
       <img src={props.show.image.medium}/>
    </Layout>
)

Post.getInitialProps = async function (context) {
  const { id } = context.query
  const res = await fetch(`https://api.tvmaze.com/shows/${id}`)
  const show = await res.json()

  console.log(`Fetched show: ${show.name}`)

  return { show }
}

export default Post

ではこのページのgetInitialPropsに注目しましょう:

Post.getInitialProps = async function (context) {
  const { id } = context.query
  const res = await fetch(`https://api.tvmaze.com/shows/${id}`)
  const show = await res.json()

  console.log(`Fetched show: ${show.name}`)

  return { show }
}

この関数の第一引数はcontextオブジェクトです。このオブジェクトはfetchに使えるクエリ情報を持っています。
この例では、まず番組のIDをクエリパラメータから引き出して、番組データをTVMaze APIからfetchしました。

このgetInitialProps関数では番組タイトルをconsole.logで表示させるコードを入れています。
クライアントとサーバの両方のコンソールを開いて下さい。
http://localhost:3000でホームページにアクセスします。そして一番上のバットマンの番組タイトルをクリックして下さい。

では、コンソールのメッセージ、つまり番組タイトルはどこに表示されたでしょう?

答えはブラウザのコンソールのみに表示されたはずです。
なぜなら、投稿詳細ページへはクライアントサイドのみでの遷移だったからです。つまり、データをfetchするにはクライアントサイドでの実行がベストです。

もし投稿詳細ページにダイレクトでアクセスした場合(例: http://localhost:3000/p/975)は、クライアントではなくサーバのみでメッセージが出力されているのが確認できるでしょう。

これでNext.jsの必要不可欠な機能はほとんど学べました。その機能とは、クライアントとサーバ両方で動く理想的なデータfetchと、サーバーサイドレンダリングです。

ほとんどのユースケースにおいて十分機能するgetInitialPropsの基礎を学びました。より詳細はNext.jsのドキュメントのdata fetchのコーナーで確認してみて下さい。

コンポーネントをスタイリングする

今まではコンポーネントをスタイリングさせることは後回しになっていました。しかし、このレッスンではスタイルを付けていく方法を学んでいきます。

Reactアプリケーションではスタイルを適用させる技術がたくさんあります。そしてそれらはおおまかには2つのカテゴリーに分けられるでしょう。

1.伝統的なCSSファイルベースのスタイリング(SASS, PostCSSを含む)
2.CSS in JSのスタイリング

伝統的なファイルベースのスタイリングは多くの問題点(特にSSRの場合)があります。なので、Next.jsでスタイリングする際にはこれは避けるように推奨しています。
そのかわり、CSSファイルをインポートするのではなく個々のコンポーネントにスタイルを付けられるCSS in JSの方法をおすすめしています。

Next.jsはstyled-jsxという、簡単に導入、スタイリングが出来ることを目的とした、CSS in JSフレームワークをプリロードしています。
おなじみのCSSの書き方でコンポーネントにスタイリングできます:書かれたCSSは別のコンポーネントには影響を与えないようになっています。(子コンポーネントに対してもです。)

つまりはスコープが限定されたCSSが書けるということです。

環境構築

変更をリセットして、サーバの再起動までしましょう。

shell
git clone https://github.com/arunoda/learnnextjs-demo.git
cd learnnextjs-demo
git checkout clean-urls-ssr
npm install
npm run dev

ホームページをスタイリングする

ホームページをスタイリングしていきましょう。
pages/index.jsを下記内容に書き換えて下さい:

pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'

function getPosts () {
  return [
    { id: 'hello-nextjs', title: 'Hello Next.js'},
    { id: 'learn-nextjs', title: 'Learn Next.js is awesome'},
    { id: 'deploy-nextjs', title: 'Deploy apps with ZEIT'},
  ]
}

export default () => (
  <Layout>
    <h1>My Blog</h1>
    <ul>
      {getPosts().map((post) => (
        <li key={post.id}>
          <Link as={`/p/${post.id}`} href={`/post?title=${post.title}`}>
            <a>{post.title}</a>
          </Link>
        </li>
      ))}
    </ul>
    <style jsx>{`
      h1, a {
        font-family: "Arial";
      }

      ul {
        padding: 0;
      }

      li {
        list-style: none;
        margin: 5px 0;
      }

      a {
        text-decoration: none;
        color: blue;
      }

      a:hover {
        opacity: 0.6;
      }
    `}</style>
  </Layout>
)

<style jsx>要素を見て下さい。ここにCSSを書いています。
このコードに差し替えたあとはこのような画面になっているはずです。

styled home page

styleタグの中には直接はスタイルを書いているわけではありません。むしろテンプレート文字列として書かれています。

テンプレート文字列をなくして、下記のようにCSSを直接書いてみましょう。

<style jsx>
  h1, a {
    font-family: "Arial";
  }

  ul {
    padding: 0;
  }

  li {
    list-style: none;
    margin: 5px 0;
  }

  a {
    text-decoration: none;
    color: blue;
  }

  a:hover {
    opacity: 0.6;
  }
</style>

するとどうなるでしょう?

答えは"SyntaxError: Unexpected token"というエラーが発生します。

Styled jsxはbabelプラグインとして動きます。すべてのCSSをパースして、ビルドプロセスに組み込みます。(それによって、オーバーヘッドの時間が全くありません。)
また、styled-jsxの中に束縛を持ち込みます。この機能により、styled-jsxの中に動的に変数を入れることが出来るようになります。なのでCSSはテンプレート文字列である必要があるのです。

スタイルとネストされたコンポーネント

ではホームページに簡単な変更を加えてみましょう。このように、リンクを独立したコンポーネントにします:

pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'

function getPosts () {
  return [
    { id: 'hello-nextjs', title: 'Hello Next.js'},
    { id: 'learn-nextjs', title: 'Learn Next.js is awesome'},
    { id: 'deploy-nextjs', title: 'Deploy apps with ZEIT'},
  ]
}

const PostLink = ({ post }) => (
  <li>
    <Link as={`/p/${post.id}`} href={`/post?title=${post.title}`}>
      <a>{post.title}</a>
    </Link>
  </li>
)

export default () => (
  <Layout>
    <h1>My Blog</h1>
    <ul>
      {getPosts().map((post) => (
        <PostLink key={post.id} post={post}/>
      ))}
    </ul>
    <style jsx>{`
      h1, a {
        font-family: "Arial";
      }

      ul {
        padding: 0;
      }

      li {
        list-style: none;
        margin: 5px 0;
      }

      a {
        text-decoration: none;
        color: blue;
      }

      a:hover {
        opacity: 0.6;
      }
    `}</style>
  </Layout>
)

上記に書き換えると、スタイルにどんな変化が起こるでしょうか。

答えは、h1のスタイルは残りますが、リンクにスタイルが効かなくなります。

isolated links

ご覧の通り、子コンポーネントにはCSSが影響しなくなりました。

styled-jsxのこの機能は、大きいアプリケーションを作る際に、スタイルを管理する助けになってくれるでしょう。

このような場合、子コンポーネントに対して直接スタイルを当てなくてっはいけません。今回はPostLinkコンポーネントに当てる必要があります:

const PostLink = ({ post }) => (
  <li>
    <Link as={`/p/${post.id}`} href={`/post?title=${post.title}`}>
      <a>{post.title}</a>
    </Link>
    <style jsx>{`
      li {
        list-style: none;
        margin: 5px 0;
      }

      a {
        text-decoration: none;
        color: blue;
        font-family: "Arial";
      }

      a:hover {
        opacity: 0.6;
      }
    `}</style>
  </li>
)

一方で、グローバルにCSSを当てたい場合はこちらの方法で可能です。

グローバルのスタイル

ときには子コンポーネントの中のスタイルを変更する必要が出てきます。これはReactでマークダウンを使っていると特にあると思います。投稿詳細ページを見て下さい。pages/post.js

これがグローバルなスタイルを簡単に適用させる例です。styled-jsxでグローバルなCSSを追加する前に、以下の内容をpages/post.jsに適用させてください。
また、こちらに書き換える前に、react-markdownコンポーネントをインストールしましょう。npm install --save react-markdown

pages/post.js
import Layout from '../components/MyLayout.js'
import {withRouter} from 'next/router'
import Markdown from 'react-markdown'

export default withRouter((props) => (
  <Layout>
   <h1>{props.router.query.title}</h1>
   <div className="markdown">
     <Markdown source={`
This is our blog post.
Yes. We can have a [link](/link).
And we can have a title as well.

### This is a title

And here's the content.
     `}/>
   </div>
   <style jsx global>{`
     .markdown {
       font-family: 'Arial';
     }

     .markdown a {
       text-decoration: none;
       color: blue;
     }

     .markdown a:hover {
       opacity: 0.6;
     }

     .markdown h3 {
       margin: 0;
       padding: 0;
       text-transform: uppercase;
     }
  `}</style>
  </Layout>
))

では、この変更で何が起こるか。
スタイルがマークダウンの内容にも適用されたはずです。

この機能が簡単に使える一方で、常にスコープが限定されたスタイルで書くことに努めるようにおすすめしています。

styled-jsxは通常のstyleタグの問題を解決する素晴らしい方法です。styled-jsxによって、babelプラグインの中でCSSバリデーションが行われ、クラス名に接頭辞が全てに付いた状態で出力されるため、無駄な時間はとられないはずです。

ここまではstyled-jsxの一面をなぞっただけで、他にも出来ることがたくさん用意されています。こちらに詳細がありますのでリファレンスとして使って下さい。
また、他にも素晴らしいスタイリング方法をNext.jsではたくさん用意しています。こちらもチェックしてみて下さい。

Next.jsアプリケーションをデプロイする

ではどうやってNext.jsアプリケーションをデプロイすればいいだろう、と思いませんでしたか?
まだ触れていませんでしたが、それは非常にシンプルで簡単です。

Next.jsアプリケーションはNode.jsが走る環境ならどこでもデプロイできます。なので、どのプラットフォームにもロックインが発生しないのですが、▲ZEIT Nowでデプロイする方法がsuper simpleでおすすめです。

環境構築

変更をリセットして、サーバの再起動までしましょう。

shell
git clone https://github.com/arunoda/learnnextjs-demo.git
cd learnnextjs-demo
git checkout using-shared-components
npm install
npm run dev

ビルドと起動

まず最初にNext.jsアプリケーションをプロダクションのためにビルドしなくてはいけません。プロダクションに最適化されたソースコードに変換されます。

では、npm scriptに簡単に以下を追加します:

package.json
"scripts": {
  "build": "next build"
}

次に、ポートを使ってNext.jsアプリケーションをスタートする必要があります。つまり、サーバーサイドレンダリングをし、上記コマンドでビルドされたページを配信します。
"start": "next start"をscriptsに追加します。

package.json
"scripts": {
  "build": "next build"
  "start": "next start"
}

3000番のポートでアプリケーションが立ち上がります。

以下コマンドを実行するとプロダクションの状態でアプリケーションを走らせることができるようになりました:

shell
npm run build
npm run start

2つのインスタンスでアプリケーションを走らせる

次に、アプリケーションを2つ走らせましょう。アプリケーションは水平スケールさせるのが普通です。
最初に、npm scriptに以下の変更をしましょう。

package.json
"scripts": {
  "start": "next start -p $PORT"
}

もしWindowsをお使いの方はnext start -p %PORT%という風に修正してください。

ではビルドします。

shell
npm run build

次に以下のコマンドをターミナルで実行してみましょう:

shell
PORT=8000 npm start
PORT=9000 npm start

Windowsでは、違うコマンドを実行する必要があります。ひとつはnpmモジュールcross-envをインストールする方法です。インストールしたらcross-env PORT=9000 npm startで走らせましょう。

では、両方のポートでアプリケーションは走ったでしょうか?
答えはYesです。
おわかりの通り、必要なビルドは一回だけです。あとはお望みのポートでたくさんのアプリケーションを走らせることができます。

▲ZEIT Nowにデプロイする

Next.jsアプリケーションをビルドしてスタートする方法を学びました。すべてnpm scriptsで完結しています。なので、お好きなデプロイサービスでカスタマイズが可能です。

▲ZEIT Nowを使用し、以下のスクリプトをpackage.jsonファイルに記載します。

package.json
"scripts": {
  "build": "next build",
  "start": "next start -p 8000"
}

次に、now.jsonというファイルをプロジェクトのルートディレクトリに以下の内容で作成します。

now.json
{
  "version": 2,
  "builds": [
    { "src": "package.json", "use": "@now/next" }
  ]
}

最後に、Nowをインストールし、以下のコマンドを実行します:

shell
now

要するに、ターミナルを使って、アプリケーションのルートディレクトリでnowと打つということです。

ここでは8000番のポートでアプリケーションをスタートしました。しかし、ZEIT Nowにデプロイする際には何も変えていません。
では、ZEIT Nowにデプロイされたアプリケーションには、どのポートでアクセスできるでしょうか。

答えは443、もしくは何もポートを指定しない、です。

8000番のポートでアプリをスタートしても、nowにデプロイされれば443番のポートでアクセスできるようになります。(httpsのデフォルトのポート番号です。)

これが▲ZEIT Nowの特徴です。
どんなポート番号でも、アプリケーションをスタートするだけで、▲ZEIT Nowは常に443番にポートをマップしてくれます。

Nextのビルダー"@now/next"をさらに学習するにはZEITのドキュメントを御覧ください。

ローカルでアプリケーションをビルドする

▲ZEIT Nowは自身のインフラでアプリケーションをビルドします。
しかし、すべてのホスティングサービスがそうなっているわけではありません。その場合に、アプリケーションをローカルでビルドできるようになっています:

shell
npm run build

これで.nextディレクトリにアプリケーションがデプロイされます。

これでNext.jsアプリケーションを▲ZEIT Nowにデプロイする方法が分かりました。
また、より詳しい情報はdeploying Next.jsのドキュメントで学べます。

もしデプロイに関して疑問がありましたら、Spectrumで気軽に質問して下さい。

おわりに

以上がNext.jsでは公式が提供するチュートリアルの"BASICS"でした。
続きに"EXCEL"があり、そこではHTMLの静的書き出し、モジュールのLazy Loading、コンポーネントのLazy Loadingが学べます。