Hirooooo’s Labo

開発メモ、ガジェット、日記、趣味など、思った事を思ったまんま書くブログ

React × Redux 初心者入門(3日目:react-router-reduxでルーティングを実装する)

f:id:hirooooo-lab:20160713002241j:plain

今回はページングの処理を実装していきたいと思います。
前回の状態からサンプルページをいくつか追加し、routerによるページングができるところまでが今回のゴールです。

前回の記事

www.hirooooo-lab.com

使用するパッケージ

react,redux環境でページングを実現するために、いくつかパッケージを追加したいと思います。
react-routerを使用し、さらにredux環境下でstate管理を行うためのパッケージとしてreact-router-reduxを採用しました。

react-router-redux以外にもredux-routerとかもあるようですが、とりあえず私はシンプルっぽいreact-router-reduxにしました。

そのあたりのrouterについては下記記事が参考になりました。

qiita.com

パッケージのインストール

ターミナルから以下のnpmコマンドでパッケージをインストールします。
今回インストールするパッケージは以下となります。

  • react-router
  • react-router-redux
  • history
$ npm install --save react-router react-router-redux history

完成予定

  --Login
  --SignUp
       +---SignUpChild1
       +---SignUpChild2
  --Home
  --Counter

こんな感じでログインページ、サインアップページ、メニューページ、カウンターページを作ってみたいと思います。
サインアップページには、子ページを2つ用意して、URLパラメータで切り替わるようにしたいと思います。
また、今回はページングの実装のみなので、中身については実装しません。

containerの作成

ページングで切り替わる対象とするコンポーネントを作成します。
とりあえずはなh1タグだけが記載されているページで枠だけ作成したいと思います。

containers/Login.jsxの作成

http://localhost:8080/loginで表示される用のページです。

containers/Login.jsxの全体像

import React { Component, PropTypes } from "react";
import { connect } from 'react-redux';

class Login extends Component {
  render() {
    return (
      <div>
        <h1>ログインページです</h1>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
  };
}

function mapDispatchToProps(dispatch) {
  return {
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Login);

同様にHome、SignUp、SignUpChild1、SignUpChild2、CounterのcontainerComponentを作成します。

Home、SignUpChild1、SignUpChild2の作成

containers/Home.jsxの全体像

import React, { Component, PropTypes } from "react";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

class Home extends Component {
  render() {
    return (
      <div>
        <h1>HOMEページです</h1>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
  };
}

function mapDispatchToProps(dispatch) {
  return {
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Home);

Login.jsxとまったく同じでh1タグの中身とclass名だけ変えたHome.jsxです。
以下のSignUpChild1、SignUpChild2も同じように作ってください。

  • containers/signup/SignUpChild1.jsx
  • containers/signup/SignUpChild2.jsx

containers/SignUp.jsxの作成

containers/SignUp.jsxは、SignUpChild1、SignUpChild2の親コンポーネントとなる予定ですので、子コンポーネントを表示する記述を追加しておきます。

containers/SignUp.jsxの全体像

import React, { Component, PropTypes } from "react";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

class SignUp extends Component {
  render() {
    return (
      <div>
        <h1>SignUpページです</h1>
        {this.props.children}
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
  };
}

function mapDispatchToProps(dispatch) {
  return {
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(SignUp);

{this.props.children}は後述もしてますが、定義された子ルートのコンポーネントがレンダリングされます。

最後にCounterを作成します。
Counterは前回までのApp.jsxで行っている処理を移行しておきます。

containers/Counter.jsxの作成

App.jsxでpropsとひも付けていたstateとactionを同じように紐付けし、renderにはHeaderコンポーネントとCounterコンポーネントを表示させます。

containers/Counter.jsxの全体像

import React, { Component, PropTypes } from "react";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import Header from '../components/Header'
import CounterContent from '../components/Counter'
import * as CounterActions from '../actions/counter'

class Counter extends Component {

  static PropTypes = {
    value: PropTypes.number.isRequired,
    actions: PropTypes.object.isRequired
  }

  render() {
    const { value, actions } = this.props
    return (
      <div>
        <Header />
        <CounterContent value={value} actions={actions} />
      </div>
    );
  }
}

function mapStateToProps(state) {
  return state.counter
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(CounterActions, dispatch)
  }
}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter);

containers/App.jsxの修正

App.jsxは前回まではcounterアクションとcounterで使用するstoreをpropsとひも付けていましたが、今回からroutingの全体的な親となるcontainerとして使用します。

ここで、アプリの共通の処理や、テーマをchildContextに渡し込んだりしてみました。

container/App.jsxの全体像

import React, { Component, PropTypes } from "react"
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MyRawTheme from '../src/myThemeFile';

class App extends Component {
  static get childContextTypes() {
    return { muiTheme: PropTypes.object.isRequired };
  }

  getChildContext(){
    return {  muiTheme: getMuiTheme(MyRawTheme)};
  }

  render() {
    const { value, actions } = this.props;
    return (
      <div>
        {this.props.children}
      </div>
    )
  }
}

// Appコンポーネントにstateを流し込む
function mapStateToProps(state) {
  return {
  };
}

function mapDispatchToProps(dispatch) {
  return {
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App)

{this.props.children}

また{this.props.children}というのが出てきました。
この部分には後述するルーティング定義で、定義された子ルートのコンポーネントがレンダリングされます。

これで一通りのcontainerの作成は完了です。
続いてルーティングの定義を行っていきます。

ルーティングの定義

ルーティングはroutes.jsxというファイルを作成し、pathに対応するコンポーネントの定義を書いていきます。

src/routes.jsxの作成

route定義を行うために、react-routerのRouteとIndexRouteをインポートして使います。

import { Route, IndexRoute } from 'react-router';

ルーティングのURL階層を設定するために、<Route />タグと<IndexRoute />タグを使用します。
<Route />タグにはpath要素とcomponent要素があり、pathには対象のURLパスを、componentには対応するreactコンポーネントを指定します。

<IndexRoute />タグにはcomponent要素のみがあり、親RouteにマッチしたURLの場合に更にpathにマッチした子コンポーネントを探しに行きますが、マッチする子ルートが存在しなかった場合に表示されるコンポーネントを定義します。

react-routerの仕様についてはNPMサイトを見るか、下記記事がわかりやすかったので一度読んでみるといいと思います。

blog.takanabe.tokyo

src/routes.jsxの全体像

import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from '../containers/App';
import Login from '../containers/Login';
import Home from '../containers/Home';
import Counter from '../containers/Counter';
import SignUp from '../containers/SignUp';
import SignUpChild1 from '../containers/signup/SignUpChild1';
import SignUpChild2 from '../containers/signup/SignUpChild2';
import NotFound from '../containers/NotFound';

const routes = (
  <Route path="/" component={App} >
    <IndexRoute component={Login} />
    <Route path="login" component={Login}/>
    <Route path="signup" component={SignUp}>
      <IndexRoute component={SignUpChild1} />
      <Route path="/signup/child1" component={SignUpChild1}/>
      <Route path="/signup/child2" component={SignUpChild2}/>
    </Route>
    <Route path="home" component={Home} />
    <Route path="counter" component={Counter} />
    <Route path="*" component={NotFound} />
  </Route>
)

export default routes

ひとつもマッチするPathが存在しなかった場合に、NotFoundというコンポーネントを表示するように設定してます。 なので、先ほど作ったcontainerと同様にNotFoundも作成しましょう。

store/configureStore.jsxの修正

RouterのStateをRedux管理下のStoreで一元管理できるようにstore/configureStore.jsxを修正します。
react-router-reduxのrouterMiddlewareにhistoryを渡し、createStoreに渡します。

store/configureStore.jsx全体像

import { createStore, compose, applyMiddleware } from 'redux'
import { routerMiddleware } from 'react-router-redux'
import rootReducer from '../reducers'

export default function configureStore(history, initialState) {
  const store = createStore(
    rootReducer,
    initialState,
    compose(
      applyMiddleware(routerMiddleware(history))
    )
  )

  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      const nextReducer = require('../reducers')
      store.replaceReducer(nextReducer)
    })
  }

  return store
}

reducers/index.jsxの修正

reducersにもreact-router-reduxのrouterReducerを追加します。

import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux'
import counter from './counter'

const rootReducer = combineReducers({
    counter,
    routing: routerReducer
})

export default rootReducer

src/index.jsxの修正

最後にアプリケーションのエントリポイントとなるsrc/index.jsxにルーティングを組み込みます。

追加でimpotするのは以下の2行です。

import { Router, browserHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'

さらに store = configureStore()にbrowserHistory, stateを引数として追加します。

Routerで使用するhitoryはsyncHistoryWithStoreにbrowserHistory, storeを引数として渡した戻りを設定します。

多分ソース見たほうが早いです。。。

src/index.jsxの全体像

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { Router, browserHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import injectTapEventPlugin from "react-tap-event-plugin";

import configureStore from '../store/configureStore'
import routes from './routes'

injectTapEventPlugin()

let state = window.__initialState__ || 
const store = configureStore(browserHistory, state)

const history = syncHistoryWithStore(browserHistory, store)
ReactDOM.render(
  <Provider store={store}>
    <Router history={history} routes={routes} />
  </Provider>,
  document.getElementById("root")
)

ここまでで実行してみる

ターミナルから以下のnpm startを実行してみてください。

$ npm start

ルートにアクセスしてみる

正常にビルドが通ったらhttp://localhost:8080/にアクセスすると、「ログインページです。」とだけ表示されると思います。

f:id:hirooooo-lab:20160729152627p:plain

URLを変更してみる

次にhttp://localhost:8080/signupにアクセスしてみてください。

本当ならここでSignUpページですと出て欲しいところですが、Cannot GET /signupって出たと思います。

ここでハマりました。

調べたところ、webpack-dev-serverの起動引数に--history-api-fallbackを追加することで解決するようです。 ちなみに、リロードするとCannot GETになってしまう原因も同様に解決します。

なので、package.jsonのscripts . startをこのように修正します。

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --hot --inline --progress --colors --history-api-fallback",
    "build": "webpack --progress --colors"
  },

こんな感じ。

もう一度http://localhost:8080/signupにアクセスしてみてください。
今度は正常に表示されたと思います。

f:id:hirooooo-lab:20160729153210p:plain

これで基本的なページングの組み込み完了です。

ボタンで画面遷移を実装する

このままではURLを変えないと遷移ができないので、画面にページ遷移を行うボタンを追加してみます。
ただ、ページ遷移のやり方がいろいろありますので、ログイン画面からHOME画面に遷移するボタンを、いろいろなルーティングの仕方で実装してみたいと思います。

ちなみに、ボタンはmaterial-uiのRaised Buttonで実装してみます。

react-routerの

一番シンプルなのがreact-routerのタグを使います。
Linkタグをimportし、以下のようにするとリンクの作成が可能です。

import { Link } from 'react-router'

・・・省略・・・

<Link to="home">HOMEへ</Link>

Linkタグを使ってRaised Buttonで実装する場合はこうなります。

import RaisedButton from 'material-ui/RaisedButton';
import { Link } from 'react-router'

・・・省略・・・

<RaisedButton label="HOMEへ" containerElement={<Link to="home" />}/>

RaisedButtonのcontainerElementにタグを入れるだけです。

react-routerのbrowserHistoryを使う方法

historyを直接使うことで、簡単に実装できます。

import { browserHistory } from 'react-router'

・・・省略・・・

<RaisedButton label="HOMEへ" onTouchTap={() => {browserHistory.push("home")}}/>

RaisedButtonのonTouchTapイベントでfunctionとしてbrowserHistory.pushを実行しています。

react-router-reduxのrouterActionsを使う方法

react-router-reduxのrouterActionsをdispatchしてページ遷移を行う方法です。

import { routerActions } from 'react-router-redux'

・・・省略・・・

<RaisedButton label="HOMEへ" onTouchTap={() => {this.props.routerActions.push("home")}}/>

・・・省略・・・

function mapDispatchToProps(dispatch) {
  return {
    routerActions: bindActionCreators( Object.assign({}, routerActions), dispatch)
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Login);

ログイン画面に実装してみる

上記のボタンをすべてログイン画面に実装してみました。

containers/login.jsxの全体像

import React, { Component, PropTypes } from "react";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import { Link, browserHistory } from 'react-router'
import { routerActions } from 'react-router-redux'

import RaisedButton from 'material-ui/RaisedButton';

class Login extends Component {
  render() {
    return (
      <div>
        <h1>ログインページです</h1>
        <h2>↓Linkを使う方法</h2>
        <Link to="home">HOMEへ</Link><br /><br />
        <RaisedButton label="HOMEへ" containerElement={<Link to="home" />}/>
        <h2>↓browserHistoryを使う方法</h2>
        <RaisedButton label="HOMEへ" onTouchTap={() => {browserHistory.push("home")}} primary={true}/>
        <h2>↓routerActionsを使う方法</h2>
        <RaisedButton label="HOMEへ" onTouchTap={() => {this.props.routerActions.push("home")}} secondary={true}/>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
  };
}

function mapDispatchToProps(dispatch) {
  return {
    routerActions: bindActionCreators( Object.assign({}, routerActions), dispatch)
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Login);

実行結果

ソースの修正が完了したら実行してみてください。
こんな感じの画面が表示され、各ボタンを押して見ると、正常にHOME画面へページ遷移が行えたと思います。

f:id:hirooooo-lab:20160729164030p:plain

まとめ

どうにかページ遷移まで実装することができました。
reactの仕様だったり、react-routerの仕様だったり、react-router-reduxの仕様だったりと、覚えることはたくさんありすぎて、正直これで合っているのかどうかもわからない状況ですが、Try & Errorを繰り返すうちに、なんとなくわかってきたような気がします。

まだまだ覚えることも多く、遠い道のりですが、同じような問題にハマっている人の少しでも役に立てれば嬉しいです。

やっぱり手を動かくのが習得の一番の近道ですね。

参考にさせていただいたサイト

react-routerとredux-simple-routerとredux-react-routerとreact-router-reduxとredux-router - Qiita

React初心者のためのreact-routerの使い方 - ハッカーを目指す白Tのブログ

Redux routerでページ遷移を実現する – PAYFORWARD

React:React Router Tutorialを試してみました - tyoshikawa1106のブログ

次回予告

次回予告というか、あとやらなきゃいけないと思ってること

  • Materialデザインを実装する DONE!!
  • ページルーティングを実装する DONE!!
  • REST通信を実装する
  • ログイン制御のフローを実装する
  • デプロイ方法を検討、実装する