【Next.js】ReactエンジニアがNext.jsを触り始めて、なんとなく使えるようになるまでの軌跡



# まえがき
普段私は**React.js**でWebアプリを開発しているのだが、ふとサーバーサイドレンダリングで表示させてみたいと思い、Qiitaで調べた。Next.jsというまさにこれだというフレームワークを見つけ、勉強し始めた。
私が**Next.js**で開発できるようになるまでに、**これってどうすれば？**と思う箇所が多かったので、振り返りも込めてまとめることにした。

対象者は、**React.js**を理解していてサーバーサイドレンダリングで表示してみたいかたです。

# 1章 Hello World

まずはじめにHello Worldを表示させるため、[Next.js公式サイト](https://nextjs.org/docs)に飛び、ドキュメントを2、3分読んだ。

そして手を動かしはじめた。

`next`と`react`と`react-dom`をインストールし、**package.json**に`npm run dev`のスクリプトを追記、pagesフォルダを作成し、その中に**index.js**を作った。

```jsx:index.js
export default () => <div>Hello World</div>

```

余裕だった、私は完全に天狗になっていた。

「**これ`react-redux`とかインストールして、`connect`で挟めば、`redux`導入もすぐできるんじゃね？**」

こんなことを心の中で囁いていた。

次に、ルーティングについて学んだ。

pagesフォルダに新しく**about.js**を作ってやれば**localhost:3000/about**で表示できるとのことだったのでやってみた。

```jsx:about.js
export default () => <div>about page</div>

```

「**おぉ、React.jsでルーティングしようとすると、複雑でライブラリによってはversionが変わると動かないという不安定さがあったが、Next.jsはシンプルでいいね**」

なんて呟いていた。

# 2章 React.jsで使っていたライブラリの導入

とりあえず、普段よく使っていた`redux`と`material-ui`と`redux-saga`を導入してみようと思い、調べ始めた。

「**ん？\_app.jsってなんだ？\_document.jsなにそれ？**」

そう、謎ファイルのとの出会いだった。**Next.js**は普通にライブラリインストールして、**React.js**のようにしてやれば導入できますというような代物ではなかったのだ。

```jsx:_app.js
import App, { Container } from 'next/app'
import React from 'react'
import withReduxStore from '../lib/with-redux-store'
import { Provider } from 'react-redux'

class MyApp extends App {
  render () {
    const { Component, pageProps, reduxStore } = this.props
    return (
      <Container>
        <Provider store={reduxStore}>
          <Component {...pageProps} />
        </Provider>
      </Container>
    )
  }
}
export default withReduxStore(MyApp)

```

**with-redux-store**をつくっていたり、`pageProps`を使っていたりとわけがわからなかった。
どうやらページ表示時はじめに実行されるものならしい。詳しくは[「Next.jsの_app.jsと_document.js」](https://qiita.com/tetsutaroendo/items/c7171286137d963cdecf)の記事がよかったので読んでみて下さい。

どうやらライブラリを導入したい場合、この**_app.js**を書き換えることで使えるようになるみたいだ。

そして大体有名なライブラリは**Next.js**の[github](https://github.com/zeit/next.js/)のexamplesに書き換え方のサンプルが落ちていることに気がついた。


`redux`も`material-ui`も`redux-saga`もサンプルが落ちていた。

このサンプルを真似て、**_app.js**を作成し手を加えていくと`redux`を導入することができた。
※_`material-ui`の導入もできたがここでは割愛する。_

#### redux導入

`redux`、`react-redux`をインストール

pagesフォルダに**_app.js**を作成

```jsx:_app.js
import App, { Container } from 'next/app'
import React from 'react'
import withReduxStore from '../lib/with-redux-store'
import { Provider } from 'react-redux'

class MyApp extends App {
  render () {
    const { Component, pageProps, reduxStore } = this.props
    return (
      <Container>
        <Provider store={reduxStore}>
          <Component {...pageProps} />
        </Provider>
      </Container>
    )
  }
}

export default withReduxStore(MyApp)

```

新しくlibフォルダを作成し、reduxを使えるようにラップする**with-redux-store.js**を作成した。

```jsx:with-redux-store.js
import React from 'react'
import { initializeStore } from '../store'

const isServer = typeof window === 'undefined'
const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'

function getOrCreateStore (initialState) {
  // Always make a new store if server, otherwise state is shared between requests
  if (isServer) {
    return initializeStore(initialState)
  }

  // Create store if unavailable on the client and set it on the window object
  if (!window[__NEXT_REDUX_STORE__]) {
    window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
  }
  return window[__NEXT_REDUX_STORE__]
}

export default App => {
  return class AppWithRedux extends React.Component {
    static async getInitialProps (appContext) {
      // Get or Create the store with `undefined` as initialState
      // This allows you to set a custom default initialState
      const reduxStore = getOrCreateStore()

      // Provide the store to getInitialProps of pages
      appContext.ctx.reduxStore = reduxStore

      let appProps = {}
      if (typeof App.getInitialProps === 'function') {
        appProps = await App.getInitialProps(appContext)
      }

      return {
        ...appProps,
        initialReduxState: reduxStore.getState()
      }
    }

    constructor (props) {
      super(props)
      this.reduxStore = getOrCreateStore(props.initialReduxState)
    }

    render () {
      return <App {...this.props} reduxStore={this.reduxStore} />
    }
  }
}
```

**store.js**を作成した。

今回は`text`と`count`のキーで値を保持させた。
`PLUS`と`SET_TEXT`アクションを定義し、`text`と`count`の値を変更できるようにした。

```js:store.js
import { createStore } from 'redux'

const initialState = {
  text: '',
  count: 0,
}

export const actionTypes = {
  SET_TEXT: 'SET_TEXT',
  PLUS: 'PLUS',
}

// REDUCERS
export const reducer = (state = initialState, action) => {
  switch (action.type) {
    case actionTypes.SET_TEXT:
      return Object.assign({}, state, {
        text: action.payload,
    })
    case actionTypes.PLUS:
      return Object.assign({}, state, {
        count: state.count + 1
      })
    default:
      return state
  }
}

// ACTIONS
export const SetTextAction = (text) => {
    return { type: actionTypes.SET_TEXT, payload: text }
}

export const PlusAction = () => {
  return{ type: actionTypes.PLUS }
}

export function initializeStore (initialState = initialState) {
  return createStore(
    reducer,
    initialState,
  )
}

```

**index.js**と**about.js**を書き換えた。

```jsx:index.js
import React from 'react'
import { connect } from 'react-redux'
import Link from 'next/link'
import { PlusAction, SetTextAction } from '../store'

class Index extends React.Component {
  static getInitialProps ({ reduxStore, req }) {
    const isServer = !!req
    if (isServer) reduxStore.dispatch(SetTextAction('hello world'))

    return {}
  }

  render () {
    return (
      <div>
        <h1>index page</h1>
        <p>text: {this.props.text}</p>
        <p>count: {this.props.count}</p>

        <button onClick={this.props.handlePlus}>plus</button>
        <button onClick={() => this.props.handleSetText('Index')}>set Index</button>

        <Link href="/about"><a>go to about page</a></Link>
      </div>
    )
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    handlePlus: () => {
      dispatch(PlusAction())
    },

    handleSetText: (text) => {
      dispatch(SetTextAction(text))
    }
  }
}

const mapStateToProps = (state) => {
  return state
}

export default connect(mapStateToProps, mapDispatchToProps)(Index)

```

```jsx:about.js
import React from 'react'
import { connect } from 'react-redux'
import { PlusAction, SetTextAction } from '../store'

class About extends React.Component {
  render () {
    return (
      <div>
        <h1>about page</h1>
        <p>text: {this.props.text}</p>
        <p>count: {this.props.count}</p>

        <button onClick={this.props.handlePlus}>plus</button>
        <button onClick={() => this.props.handleSetText('About')}>set About</button>
      </div>
    )
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    handlePlus: () => {
      dispatch(PlusAction())
    },

    handleSetText: (text) => {
      dispatch(SetTextAction(text))
    }
  }
}

const mapStateToProps = (state) => {
  return state
}

export default connect(mapStateToProps, mapDispatchToProps)(About)

```

Next.jsはどうやらはじめにサーバーサイドレンダリングで表示され、そこから先はクライアント側の処理だけで表示されるSPAに切り替わるらしい。

`getInitialProps`はページが表示されるときに、一度だけ実行されるらしい。
はじめはサーバー側で実行され、そこから先ページ遷移した場合はクライアント側で実行される万能な関数だ。
もちろんリロードすればサーバー側で実行される。

**index.js**では`hello world`の文字を`text`にいれるアクション`SetTextAction`を`dispatch`している。また`req`の値でサーバー側の処理かクライアント側の処理か判定もできた。

```jsx
class Index extends React.Component {
  static getInitialProps ({ reduxStore, req }) {
    const isServer = !!req
    if (isServer) reduxStore.dispatch(SetTextAction('hello world'))

    return {}
  }

  render () {
    // 省略
  }
}
```

「**なんか、`getInitialProps`が使えるようになったことで少しだけNext.jsがどういうものなのか理解が深まった気がした。**」

# ３章 Web APIってどうやって使う？

WebAPIの使用はとても簡単だった。
[Next.js公式サイト](https://nextjs.org/learn)のlearnのFetching Data for Pagesにて解説してあった。

もうすでにあなたは知ってる、`getInitialProps`でリクエストを投げればいい。

まず、リクエストを投げる`isomorphic-unfetch`をインストール。

**index.js**を書き直した。

```jsx:index.js

import React from 'react'
import { connect } from 'react-redux'
import fetch from 'isomorphic-unfetch'
import Link from 'next/link'
import { PlusAction, SetTextAction } from '../store'

class Index extends React.Component {
  static async getInitialProps ({ reduxStore, req }) {
    const isServer = !!req
    if (isServer) reduxStore.dispatch(SetTextAction('hello world'))

    const endpoint = 'https://qiita.com/api/v2/users/kousaku-maron/items?page=1&per_page=100'
    const res = await fetch(endpoint)
    const data = await res.json()

    const result = data.map(record => record.title)

    return {
      result: result,
    }
  }

  render () {
    return (
      <div>
        <h1>index page</h1>
        <p>text: {this.props.text}</p>
        <p>count: {this.props.count}</p>
        {this.props.result.map(title => (
          <p key={title}>{title}</p>
        ))}

        <button onClick={this.props.handlePlus}>plus</button>
        <button onClick={() => this.props.handleSetText('Index')}>set Index</button>

        <Link href="/about"><a>go to about page</a></Link>
      </div>
    )
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    handlePlus: () => {
      dispatch(PlusAction())
    },

    handleSetText: (text) => {
      dispatch(SetTextAction(text))
    }
  }
}

const mapStateToProps = (state) => {
  return state
}

export default connect(mapStateToProps, mapDispatchToProps)(Index)
```

`getInitialProps`を`async`にし、`isomorphic-unfetch`でリクエストを投げている。返り値は`this.props`に入るので利用する。

※_今回使用しているAPIはQiita APIです。_

```js
static async getInitialProps ({ reduxStore, req }) {
    const isServer = !!req
    if (isServer) reduxStore.dispatch(SetTextAction('hello world'))

    const endpoint = 'https://qiita.com/api/v2/users/kousaku-maron/items?page=1&per_page=100'
    const res = await fetch(endpoint)
    const data = await res.json()

    const result = data.map(record => record.title)

    return {
      result: result,
    }
  }

```

# 4章 localhost/:idなどのルーティングはどうやる？

こちらも[Next.js公式サイト](https://nextjs.org/learn)のlearnのServer Side Support for Clean URLsにて解説してあった。

`express`を利用して実現するらしい。

`express`をインストール。

**server.js**を作成し、ルーティングの設定をした。

```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 = '/about'
    const queryParams = { id: 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)
})

```

同時に**package.json**を書き換えた。

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

```

**index.js**では、SPAによる遷移で**about.js**のページに遷移する。
**server.js**はサーバーサイドレンダリングでの処理のため、クライアント側でもクエリを取得できるように、
`Link`の属性である、`href`にはクエリを含む本来のルーティングを、`as`には表示するルーティングを記載しなければならない。

```jsx:index.js
// 省略

class Index extends React.Component {
  // 省略

  render () {
    return (
      <div>
        <h1>index page</h1>
        <p>text: {this.props.text}</p>
        <p>count: {this.props.count}</p>
        {this.props.result.map(title => (
          <p key={title}>{title}</p>
        ))}

        <button onClick={this.props.handlePlus}>plus</button>
        <button onClick={() => this.props.handleSetText('Index')}>set Index</button>

        <Link as="/p/1" href="/about?id=1"><a>go to about page</a></Link>
      </div>
    )
  }
}

// 省略

export default connect(mapStateToProps, mapDispatchToProps)(Index)

```

**about.js**でクエリを取得し表示させてみる。クエリの取得には`withRouter`を使用する。

```jsx:about.js
import React from 'react'
import { withRouter } from 'next/router'
import { connect } from 'react-redux'
import { PlusAction, SetTextAction } from '../store'

class About extends React.Component {
  render () {
    return (
      <div>
        <h1>about page</h1>
        <p>text: {this.props.text}</p>
        <p>count: {this.props.count}</p>
        <p>id: {this.props.router.query.id}</p>

        <button onClick={this.props.handlePlus}>plus</button>
        <button onClick={() => this.props.handleSetText('About')}>set About</button>
      </div>
    )
  }
}

// 省略

export default connect(mapStateToProps, mapDispatchToProps)(withRouter(About))
```

これで、サーバーから直接アクセスしても、**index.js**からSPAでアクセスしてもidの取得が可能になった。

「**サーバー側での処理とクライアント側での処理を意識しながら開発をしていかないとダメなんだなぁと実感した。**」

# あとがき

これでカスタムサーバー、ライブラリの導入ができるようになったのでNext.jsでWebアプリを作れるようになったんじゃないでしょうか。React.jsを触ったことがあるのであれば一度Next.jsを触ってみるのもいいかもしれませんね。

[サンプルコード](https://github.com/kousaku-maron/nextjs-sample-for-qiita)をgithubに公開しておきました。