読者です 読者をやめる 読者になる 読者になる

Next.js+Reduxに触れてみる

世界の皆様、こんにちは。naoya3eです。

今年の10月末にZEITからリリースされたNext.jsというフレームワークが面白そうだったので、ちょっと触ってみようかと思って調べたことや使い方のメモを残そうと思います。

Next.jsとは?

ZEITさんがリリースしたServer Side RenderingするUniversalなWebアプリケーションのためのフレームワークだそうです。ZEITさんはHyperというターミナルエミュレータだったり、NowというPaaSだったりを開発されています。

NodeでのWebアプリケーションを開発するにあたって、今までは自分しか使わないアプリを作っていたためそれほど重要視していませんでしたが、Single Page Application(SPA)での初回ロードに時間がかかる問題は深刻です。これをSSR(Server Side Rendering)によって解決しようとするアプローチがあり、この動作を担ってくれるフレームワークNext.jsなのです。

とりあえず、どういうものなのかをざっと説明した後、サンプルを作ってみようと思います。

Next.jsの特徴

Next.jsはいくつかの面白い特徴を持っています。まだ使い勝手がちょっと分からないとこや、「これできないのかー」ってとこもありますが、簡単で素敵です。

  • 導入が簡単!
  • 開発環境(Babel/Webpack等)を設定する必要がない
  • ディレクトリ構造がルーティングとして扱われる

これらの特徴は以下で説明していきます。

Next.jsの導入

Next.jsを導入するには以下のコマンドを実行するだけです。

npm install --save next

Reactを利用するアプリ開発が前提とされているっぽいですが、nextをインストールするとBabelやらReactやらのモダンな開発環境も一緒にインストールされます。つまり導入が簡単なのです。ついでにビルドの設定なども書くことなくコマンド一発でしてくれるようになっています。開発をするためにpackage.jsonのscriptにdevelopコマンドを追加します。

{
  "scripts": {
    "develop": "next"
  }
}

これでnpm run developでビルドが行われます。.babelrcとかwebpack.config.jsとかを書く必要はありません。全部nextがやってくれます。フロントエンドの開発環境の設定はめんどくさいですからね、ありがたいです。

ディレクトリ構造がなんだって?

Next.jsではpages/以下に設置したJSファイルがルーティングとして(?)扱われます。react-routerとか使わないっぽいです。

/pages/index.js          =>   /
/pages/hello.js          =>   /hello
/pages/nested/index.js   =>   /nested
/pages/nested/intro.js   =>   /nested/intro

このようにルートがディレクトリ構造にしたがってマッピングされます。index.jsがその階層のIndexRouteとして当てられるようです。ファイル名には気をつけましょう。

サンプルアプリを作ってみる

さて、そろそろ自分でNext.jsを利用したアプリを書いてみましょう。チュートリアルをそのまま書いても面白くないのでReduxを導入したカウンターアプリを作成します。最終的にはNowへのデプロイを目標とします。

サンプルはこちらで動作しています。またソースコードGithubにあげています。

まずはReduxをインストールしましょう。Reactと接続するreact-reduxもインストールします。またついでにredux-actも導入します。こちらはReduxでのActionやReducerを簡単に記述するためのものです。

npm install --save react-redux redux redux-act

続いてエントリーポイントとなるpages/index.jsを作成していきます。以下のように記述します。

// pages/index.js
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../store/configureStore';
import Counter from '../components/Counter';

export default class App extends React.Component {
  static getInitialProps({ req }) {
    const isServer = !!req;
    const store = configureStore({ counter: 0 }, isServer);
    return { initialState: store.getState(), isServer };
  }

  constructor(props) {
    super(props);
    this.store = configureStore(props.initialState, props.isServer);
  }

  render() {
    return (
      <Provider store={this.store}>
        <Counter />
      </Provider>
    );
  }
}

簡単に解説していきます。importは飛ばします。configureStoreとかCounterとかは後述します。pages/以下に作成するルートとしてマッピングされるコンポーネントはどうやらexportする必要があるようです。忘れずexport defaultしましょう。

重要なのはgetInitialProps()です。これはNext.jsによってReactコンポーネントに拡張を行います。この関数はいくつかの引数を持つことができます。引数にどのようなものがあるかはGithubのREADMEで確認できます。ここではreqを引数としています。reqはサーバー側での処理のときのみ得られます。これによってこのコンポーネントの処理がサーバー/クライアントのどちらで行われているかを判断します。SSRの開発の経験がなかったため、Reduxのstoreをどのように持てばいいのか分からず、理解するのに苦労しました。Wikiにあるサンプルでもこのように記述されているので、たぶんあってるはずです(自信ない)。

Redux Storeの設定を行う関数configureStore()に初期stateとサーバー側かどうかを引数として与えます。初回ロード時のみサーバーへのリクエストがくるはずなので初期stateを与えているという解釈でいいのでしょうか。最後にここで用意したstoreとisServerを返します。これらはコンポーネントpropsに与えられます。

コンストラクタではクライアント側でのStoreの設定を行います。render()部分は見たまんまです。


Storeの設定を書いていきます。ここではReduxのstoreやAction、Reducerはすべて分割してディレクトリを分けて管理します。store/configureStore.jsを作成します。

// store/configureStore.js
import { createStore } from 'redux';
import reducer from '../reducers';

export default function configureStore(initialState, isServer) {
  if (isServer && typeof window === 'undefined') {
    return createStore(reducer, initialState);
  } else {
    if (!window.store) {
      window.store = createStore(reducer, initialState);
    }
    return window.store;
  }
}

ここの記述はWikiにあるReduxのサンプルそのままですね。reducerを引数としてもらってないことぐらいが違いです。クライアント側でStoreがwindow.storeになければStoreを作ってやる、という動作をしています。


それではカウンターの動作部分となるActionとReducerを書いていきます。まずはActionからです。actions/index.jsを作成します。

// actions/index.js
import { createAction } from 'redux-act';

export const increment = createAction('increment');
export const decrement = createAction('decrement');

redux-actを使わない人には見慣れないかもしれませんが、createAction()はActionCreatorを返す関数でオブジェクトが返ってきます。普通にredux-actを使わずに書いてもあまり変わらないっちゃ変わらないですね...。

次にReducerを準備します。reducers/index.jsを作成します。

// reducers/index.js
import { createReducer } from 'redux-act';
import { increment, decrement } from '../actions';

const initialState = {
  counter: 0,
}

export default createReducer({
  [increment]: (state) => ({ counter: state.counter + 1}),
  [decrement]: (state) => ({ counter: state.counter - 1}),
}, initialState);

さきほど作成したActionCreatorをimportします。redux-actcreateReducerはちょっと分かりにくいかもしれませんが、switch (action.type)って書いてたところをたぶんComputed property namesで動かしてる...んじゃないかと思ってます。ここの動きがちょっとよくわかりません。教えてえらい人!(本題のNext.jsと関係ないですけど)

動作としてはstateが持っているcounterをincrement/decrementして新しいオブジェクトとして返します。ここのArrow functionで(state) => {}として丸括弧を忘れていたせいでハマりました、馬鹿です。


疲れてきましたが、いよいよ最後です。カウンター部分のコンポーネントを作成していきます。components/Counter.jsを作成します。

// components/Counter.js
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from '../actions';
import HeadElements from './HeadElements';

class Counter extends React.Component {
  render() {
    return (
      <div>
        <HeadElements />
        <h2>{this.props.counter}</h2>
        <button onClick={() => this.props.dispatch(increment())}> + </button>
        <button onClick={() => this.props.dispatch(decrement())}> - </button>
      </div>
    );
  }
}

function mapStateToProps({ counter }) {
  return { counter };
}

export default connect(mapStateToProps)(Counter);

このコンポーネントではReactとReduxをconnectし、Storeからデータを得ます。mapStateToProps()によってstate.counterをCounterコンポーネントpropsとして与えます。またActionをimportし、ボタンのonClick時にdispatchするように記述します。

HeadElementsというコンポーネントはHTMLの<head>タグを書き換えるものなので気にしないでください。プログラム部分とはほぼ関係ありません。Next.jsでは<head>タグの書き換えも行えるので気になる方はREADMEを読むといいと思います。


こんなとこでしょうか。だらだらと書いてしまい長くなってしまいましたが、これで動くようになっているはずです。説明ベタであれですが、ソースコードを読んでいただければ、だいたいは理解できると思います。

実行してみる

それではローカルでアプリを実行してみましょう。

npm run develop

Next.jsはビルドの設定などを勝手にやってくれるので楽でいいですね。個人的にwebpackでビルドを行うとメッセージが見にくくてあまり好きじゃないのですが、綺麗にメッセージを表示してくれます。コンパイルが終わると以下のようにメッセージが表示されるはずです。

DONE  Compiled successfully in 2000ms

> Ready on http://localhost:3000

コンパイルは成功しており、localhostの3000番ポートにアプリが起動しています。コンパイルに失敗していればエラーの表示が出力されます。

ブラウザでlocalhost:3000を開くと、カウンターが表示されているはずです。+や-を押してみて動くか確認します。

Nowにデプロイしてみる

Nowにデプロイしています。Nowをインストールして実行するだけでURLが割り当てられ、アプリを公開することができます。

npm install -g now
now

Nowを初めて使うときだけ(?)、メールでの認証が必要となります。これであとは勝手にやってくれます。すごいですね!

私が作成したこのカウンターアプリはこちらで公開されています。ソースコードGithubにあげていますので、参考にしてくださると幸いです。

最後に

Next.jsに触れる記事にするつもりだったのに、なんだかだらだらと書いてしまいました。分かりにくいかもしれません。ほんとはReact+ReduxをWebpackでっていう記事を書いてからNext.jsとか触ってみて記事を書こうとしていたのですが、ついつい手が出てしまいました。そのうちNowのことととかも調べて記事にしたいですね。

Next.jsは面白いフレームワークでしたし、何より開発環境を用意してくれるのは素晴らしいです。Nowによるデプロイまで流れが簡単で感激しました。サンプルのコードはお粗末ですが、なにか問題点とかありましたら教えていただけると幸いです。

サンプルのリポジトリを公開してから記事を書いてたらこんな年末になってしまいました...。 記事を書くのは大変なようです。それでは皆さん、良いお年を。

参考