この記事はQiita React.js Advent Calendar 2015の18日目に投稿させて頂いた記事です。

モチベーション

reduxのページ切り替えにreact routerを導入したい

成果物

redux-routing-example/containers at master · takanabe/redux-routing-example · GitHubにredux-routerを使ったサンプルプロジェクトを置いておく。

redux-routerとは

redxu-routerreact-routerのAPIをReduxから利用出来るようにするもの。

Reduxではステートを1つのStoreで管理する規則があるが、redux-routerを使うことで、RouterもStoreで管理できるようになる。

ルーティングの例

ルーティングの設定はreact-routerと同じ。例えば、以下のようなルーティングの設定がされているとする。

<Route  path="/" component={Parent} >
  <IndexRoute component={ChildA} />
  <Route path="child_a" component={ChildA} />
  <Route path="/child_b/:id" component={ChildB} >
    <Route path="gchild_a" component={GrandChildA} />
    <Route path="gchild_b" component={GrandChildB} />
  </Route>
  <Route path="*" component={NotFound} />
</Route>

この時、アクセスするパスでRouterはレンダリングするComponentを決定する。

”/“にアクセスした場合

  1. Parent Componentをレンダリング対象にする
  2. 子階層を見に行き、pathにマッチしているComponentを探す
  3. pathにマッチするものがないのでIndex Routeに設定されているChildA Componentをレンダリング対象にする
  4. ChildA Componentに子階層がないためルーティングを終了する

”/child_aにアクセスした場合

  1. Parent Componentをレンダリング対象にする
  2. 子階層を見に行き、pathにマッチしているComponentを探す
  3. pathがChildAのpathとマッチしているのでChildA Componentをレンダリング対象にする
  4. ChildA Componentに子階層がないためルーティングを終了する

”/child_a/xxxx”にアクセスした場合

  1. Parent Componentをレンダリング対象にする
  2. 子階層を見に行き、pathにマッチしているComponentを探す
  3. pathが*にマッチするのでNotFound Componentをレンダリング対象にする
  4. NotFound Componentに子階層がないためルーティングを終了する

”/child_b/xxxx”にアクセスした場合

  1. Parent Componentをレンダリング対象にする
  2. 子階層を見に行き、pathにマッチしているComponentを探す
  3. pathがChildBのpathとマッチしているのでChildB Componentをレンダリング対象にする
  4. 子階層を見に行き、pathにマッチしているComponentを探す
  5. pathにマッチするものがなく、Default Routeに設定されているGrandChildAをレンダリング対象にする

”/child_b/xxxx/gchild_b”にアクセスした場合

  1. Parent Componentをレンダリング対象にする
  2. 子階層を見に行き、pathにマッチしているComponentを探す
  3. pathがChild-BのpathとマッチしているのでChild-B Componentをレンダリング対象にする
  4. 子階層を見に行き、pathにマッチしているComponentを探す
  5. pathにマッチしているのでGrandChild-Bをレンダリング対象にする

ルーティングを利用してComponentを入れ子にする

Componentを入れ子にしてパスによってレンダリングする子Componentを定義することっもできる。

import React from 'react';
import { Route } from 'react-router';
import App from '../containers/App';
import Search from '../containers/Search';
import Register from '../containers/Register';
import Usage from '../containers/Usage';
import NotFound from '../containers/NotFound.jsx';

export default (
<Route  path="/" component={App} >
  <IndexRoute component={Register} />
  <Route path="registration" component={Register} />
  <Route path="search" component={Search} />
  <Route path="usage" component={Usage} />
  <Route path="*" component={NotFound} />
</Route>
);

親コンポーネントでは{children}を利用する。

# App.jsx
import React, { Component, PropTypes } from "react";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { pushState } from 'redux-router';
import Header from '../components/Header';
import Footer from '../components/Footer';

class App extends Component {

  render() {
    return (
      <div>
        <Header />
        <div>
          {this.props.children}
        </div>
        <Footer />
      </div>
    );
  }
}

App.propTypes = {
};

function mapStateToProps(state) {
  return {
  };
}

function mapDispatchToProps(dispatch) {
  return {
  };
}

export default connect(
  mapStateToProps,
  {pushState}
)(App);

実例

準備

takanabe/react-redux-material_ui-boilerplate · GitHubのboilerplateを利用してベースとなるアプリケーション環境の準備をする。

Step1 App Componentのルーティングの設定

全てのComponentを内包するApp Componentを編集する。

import React, { Component, PropTypes } from "react";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Header from '../components/Header';
import Footer from '../components/Footer';

class App extends Component {
  render() {
    return (
      <div>
        <Header />
        <Footer />
      </div>
    );
  }
}


App.propTypes = {
};

function mapStateToProps(state) {
  return {
  };
}

function mapDispatchToProps(dispatch) {
  return {
  };
}

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

同様にHeader、Footer Componentの中身も変更しておく。

# component/Header.jsx
import React, { PropTypes, Component } from 'react';
import mui, {AppBar} from 'material-ui';
const ThemeManager = require('material-ui/lib/styles/theme-manager');
import MyRawTheme from '../src/material_ui_raw_theme_file'

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

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

  render() {
    return (
      <header className="header">
          <AppBar title="Header: Redux router example" />
      </header>
    );
  }
}

Header.propTypes = {
};

export default Header;


# component/Footer.jsx
import React, { PropTypes, Component } from 'react';


class Footer extends Component {
  render() {
    return (
      <footer className="footer">
        <p>Footer</p>
      </footer>
    );
  }
}

Footer.propTypes = {
};

export default Footer;

RouterのStateをRedux Storeで管理できるようにstore/configureStore.jsxを以下のように編集する。

# 変更前
import { createStore } from 'redux';
import rootReducer from '../reducers';

export default function configureStore(initialState) {
  const store = createStore(rootReducer, initialState);

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

  return store;
}

# 変更後
import routes from '../src/routes';
import { reduxReactRouter } from 'redux-router';
import createHistory from 'history/lib/createBrowserHistory';
import { createStore, compose} from 'redux';
import rootReducer from '../reducers';

const finalCrateStore = compose(
  reduxReactRouter({ routes, createHistory })
)(createStore);

export default function configureStore(initialState) {
  const store = finalCrateStore(rootReducer, initialState);

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

  return store;
}

また、reducer/index.jsxを編集してReducerにもrouterStateReducerを導入する。

import { combineReducers } from 'redux';
import { routerStateReducer as router } from 'redux-router';

const rootReducer = combineReducers({
    router
});

export default rootReducer;

App Componentを表示させるためにルーティングの設定ファイルsrc/router.jsxを作成する。

import React from 'react';
import { Route } from 'react-router';
import App from '../containers/App';

export default (
  <Route path="/" component={App}>
  </Route>
);

最後に、src/index.jsxでReduxRouter Componentをレンダリングするように編集する。

import React from "react";
import ReactDOM from "react-dom";
import injectTapEventPlugin from "react-tap-event-plugin";
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { ReduxRouter } from 'redux-router';

import configureStore from '../store/configureStore';

window.React = React;

injectTapEventPlugin();

const store = configureStore();

ReactDOM.render(
  <Provider store={store}>
    <ReduxRouter />
  </Provider>,
  document.getElementById("root")
);

Step2 Child-A,Child-B Componentのルーティングの設定

Child-A,Child-B Componentを作成する。。

# container/Child-A.jsx
import React, { Component, PropTypes } from "react";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

class ChildA extends Component {
  render() {
    return (
      <div>
        <h1> Child A Component</h1>
      </div>
    );
  }
}


App.propTypes = {
};

function mapStateToProps(state) {
  return {
  };
}

function mapDispatchToProps(dispatch) {
  return {
  };
}

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


# container/Child-B.jsx
import React, { Component, PropTypes } from "react";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

class ChildB extends Component {
  render() {
    return (
      <div>
        <h1> Child B Component</h1>
      </div>
    );
  }
}


App.propTypes = {
};

function mapStateToProps(state) {
  return {
  };
}

function mapDispatchToProps(dispatch) {
  return {
  };
}

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

Child-A,Child-B Componentを表示させるためにルーティングの設定ファイルsrc/router.jsxを作成する。

import React from 'react';
import { Route } from 'react-router';
import Parent from '../containers/App';
import ChildA from '../containers/Child-A.jsx';
import ChildB from '../containers/Child-B.jsx';

export default (

  <Route name="Parent" component={Parent} path="/">
    <DefaultRoute component={ChildA} />
    <Route name="child_a" component={ChildA} path="child_a" />
    <Route name="child_b" component={ChildB} path="child_b/:id" />
  </Route>
);

終わり。

参考

react-routerについて

redux-routerについて