概要
前回までのアプリケーションにルーティングを追加する.ルーティングとは,いわゆる画面遷移とほぼ等価で.SPAになる前のWebアプリケーションで,リンクをクリックするとページ全体が書き換わる処理をSPAで実行する仕組みのことを指す.これを実現するライブラリとしてreact-router
が有名なので,それを使ってルーティング機能を組みこんでみる.
準備
まず,react-router
をインストールする
>npm install --save react-router
これをインストールすると,BrowswerRouter
, HashRouter
, Link
, Route
などのコンポーネントが使えるようになる.それぞれのコンポーネントを概説する.その前に.SPAのルーティングがどのように行われているか整理してみる.SPAのルーティングでも,URLに関連付けて管理したほうが都合が良い(管理しやすい).しかし,実際にはSPAなので,画面全体の再描画はしてほしくない.そこで,react-router
では,ブラウザの historyAPI を利用してうまくやっている.どのようにうまくやっているか現時点で正確には理解していないが,とにかく1つのURLに対して1画面を対応付ける,というところを抑えておけば良いと思う.
BrowserRouter: historyAPIを用いてルーティングを管理するコンポーネント.このコンポーネントを使うと,例えば "http://xxx.com/" に対して
App
, "http://xxx.com/second" に対してSecond
のようにURLとコンポーネントを対応付けることができる.ただ.URLを指定するとブラウザの仕様上サーバにHTTPリクエストが送信されてしまう.そこで,404とかになるとダメなので,このコンポーネントを使う場合はサーバサイドで有効なコンテンツを返すような準備が必要となる(どうやるかは後日調査)HashRouter:同じくhistoryAPIを用いてルーティングを管理するコンポーネント.BrowserRouterでは1つのURLに対して1つのコンポーネントを対応付けるが,HashRouterではページ内遷移であるハッシュ("#")を利用して関連付ける.例えば,先ほどの例では "http://xxx.com/#" で
App
, "http://xxx.com/#/second" でSecond
のように関連付ける.なお,"#"をつけるのはこのコンポーネントが勝手にやってくれるので,利用者が意識する必要はない.Route: URLとコンポーネントを対応付けるコンポーネント.詳細は具体例を参照のこと
Link: ルーティングを実行するコンポーネント.要するに.URLにジャンプするリンクを作成する.
react-router
ではこの他にもコンポーネントを提供しているが,ここでは割愛.
次に,これまでのアプリケーションに切り替えて表示するコンポーネントを追加する.
import React from 'react';
export default (props) => {
return (
<h2>Hello Second</h2>
);
}
ルーティングの確認がしたいだけなので,これで十分.
BrowserRouter/HashRouterを用いたルーティングの実装
BrowserRouterを使ったルーティング
ルーティングはルートであるindex.js
に導入する.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import appReducer from './reducers/items';
import App from './components/App';
import Second from './components/Second';
import {BrowserRouter, HashRouter, Route, Link} from 'react-router-dom'; // (1)コンポーネントのインポート
import {createStore} from 'redux';
import {Provider} from 'react-redux';
const store = createStore(appReducer);
function renderApp() {
ReactDOM.render(
<Provider store={store}>
<BrowserRouter> // (2) BrowserRouterの導入
<div>
<ul>
<li><Link to='/'>App</Link></li> // (3) リンクの作成
<li><Link to='/second'>Second</Link></li>
</ul>
<hr/>
<Route exact path='/' component={App} /> // (4)URLとコンポーネントのペア
<Route exact path='/second' component={Second} />
</div>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
}
renderApp();
(1)
4つのコンポーネントをreact-router-dom
からインポートする.
(2)
BrowserRoute
を配置する.既にRedux
を導入しているので,Provider
の配下に配置する.
(3)
Link
コンポーネントを使って,リンクを作成している.この例では,例えば"App"という文字をクリックすると."http://xxx.com/" にジャンプするようになる.
(4)
Route
コンポーネントを使ってpath
と表示するコンポーネントのペアを作成している.この例から明らかのように,"/"にアクセスされればApp
が."/second"にアクセスされればSecond
が表示されるようになる.なお,Route
のexact
属性は,パスが完全一致した時のみルーティングされる,という意味である.exact=true
であるが,exact
がbooleanなので,true
は省略可能.
HashRouterを使ったルーティング
HashRouterを使った場合は以下のようになる.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import appReducer from './reducers/items';
import App from './components/App';
import Second from './components/Second';
import {BrowserRouter, HashRouter, Route, Link} from 'react-router-dom';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
const store = createStore(appReducer);
function renderApp() {
ReactDOM.render(
<Provider store={store}>
<HashRouter> // ここだけ違う
<div>
<ul>
<li><Link to='/'>App</Link></li>
<li><Link to='/second'>Second</Link></li>
</ul>
<hr/>
<Route exact path='/' component={App} />
<Route exact path='/second' component={Second} />
</div>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
}
renderApp();
見ればわかるように,BrowserRouter
と書いていた部分をHashRouter
と置き換えるだけ.実際に実行してみると(npm start
),ブラウザのURLが "http://localhost:3000/" とか,"http://localhost:3000/#/second" になり,ハッシュ(#)を使っていることがわかる.
このように,ハッシュを使ったURLの生成は.HashRouter
がよろしくやってくれる.
React-Router-Reduxを利用したルーティング
ここまでの方法では,ルーティングするために
-
Route
によるpathとコンポーネントのペアを定義 -
Link
によってリンクを作成
してルーティングを実現してきた.Link
のto
属性に指定するのは特定の文字列であるため,実行結果に応じてルーティング先を切り替える,つまりプログラムで実行時にルーティングを切り替えるようなことができない.そこで,React-Router-Reduxでは,ルーティング情報をStoreのステートで管理しつつ.'push'や'replace'のようなメソッドを利用してルーティングするURLを変更する手段を提供する.
準備
React-Router-ReduxとHistoryをインストールする
>npm install --save react-router-redux@next
>npm install --save history
react-router-redux
は5系をインストールする必要があるので,@next
をインストールする.なお,npm
でリリースされているライブラリのバージョンを確認するには,
>npm info react-router-redux version
を実行して確かめると良い.ここで4系だったら@next
(バージョン指定)をつける.
なお, インストール済み のライブラリのバージョンを確認するには,
>npm list --depth=0
とするとよい.
Reducerの合成とActionによるルーティングの実行
React-Router-Reduxでは,ルーティングを行うためのrouterReducer
というReducerを持っている.ここまで作ってきたアプリケーションでは,(a)既にreducer/item.js
にappReducer
を定義しているので,両者を合成する必要がある.
次に,(b)historyをStoreで管理してルーティングする必要がある.
最後に,Link
などのコンポーネントを使うだけでなく,(c)アクションで実行するプログラムによって,動的にルーティングを実行したい.(a)~(c)を実現するには,それぞれ
(a). combineReducer
の実行
(b). ConnectedRouter
の適用
(c). ミドルウェア(routerMiddleware
)の適用
をそれぞれ行う必要がある.
combineReducerの実行
combineReducer
は,名前の通り複数のReducerを合成する関数である.combineReducer
は,引数に複数のReducerを内包したJavaScriptのオブジェクトを受け取る.
combineReducer({
apps: appReducer,
router: routerReducer,
})
この場合,これまで利用していたappReducer
と,React-Router-Reduxが提供するrouterReducer
を合成し,関数を返す.
例えばこの結果返ってくる関数は,以下のようになる.
ƒ combination() {
var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var action = arguments[1];
if (shapeAssertionError) {
throw shapeAssertionError;…
この後,この結果(生成された関数)を利用してStore
を生成するが,store.getState()
としてステートを確認すると,
{
apps: {task: "", tasks: Array(0)}
router : {location: null}
__proto__
:
Object
}
となっている.これは,combineReducer
で複数のReducerを合成すると,それぞれのReducerが管理するステートが混在してしまうため,combineReducer
に渡した複数のreducerを内包するオブジェクトの名前(appsやrouter)を使って,combineReducer
が1つのステートにマージした,ということである.大事なところ(だと思う)ので,もう一度何でこんなことをする必要があるのか,ということを考えると,
- Reducerの役割はStateとActionを受け取って新しいStateオブジェクトを返すことである
- Reduxでは通常アプリケーション全体で1つのStateを扱う.そのため,原則として1つのReducerに対して1つのStateがあるべきである.
- 複数Reducerが存在する場合,複数のStateが存在することになってしまい都合が悪い
- よって,
combineReducer
でそれぞれのReducerで管理するStateに名前をつけ,更にそれをまとめて1つのStateとすることで, 1つしかStateオブジェクトは存在しないというReduxの原則を守りつつ.各Reducerは自身が管理するStateしか知らなくてよい ということを実現する.
という手順で整理することで納得することができると思う.つまり,combineReducer
をすることで,擬似的に複数のStateが存在するような感じになる.
ConnectedRouterの適用
これまで利用していたBrowserRouter
やHashRouter
の代わりにConnectedRouter
を適用する.ConnectedRouter
はhistoryを利用してルーティングを管理するため,historyを生成し,Props経由で渡す必要がある.
import createBrowserHistory from 'history/createBrowserHistory';
(省略)
const history = createBrowserHistory();
(省略)
<Provider store={store}>
<ConnectedRouter history={history}>
(省略)
このように,ConnectedRouterはProviderの直下に配置する.
Middlewareの適用
routerMiddleware
をインポートし,さらにReduxが提供するapplyMiddleWare
関数の引数に実行結果のオブジェクトを渡すことでミドルウェアを適用できる.具体的には以下のようになる.
import { applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
(省略)
applyMiddleware(routerMiddleware(history))
この返り値は以下のような関数である.
ƒ (createStore) {
return function () {
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
var …
なお,複数のミドルウェアを適用する場合には,以下のようにすればよい
let middleware = [a, b];
applyMiddleware(...middleware);
複数のReducerとミドルウェアを適用したStoreの生成
複数のReducerとミドルウェアを適用してStoreを生成するには.以下のようにする.
const store = createStore(
combineReducers({..}),
applyMiddleware(...middleware)
)
以上を踏まえ,複数Reducerとミドルウェアを適用した後のindex.js全体を示す.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import appReducer from './reducers/items';
import App from './components/App';
import Second from './components/Second';
import {BrowserRouter, HashRouter, Route, Link} from 'react-router-dom';
import { createStore,
combineReducers,
applyMiddleware
} from 'redux';
import {routerReducer, routerMiddleware } from 'react-router-redux';
import {ConnectedRouter} from 'react-router-redux';
import createBrowserHistory from 'history/createBrowserHistory';
import {Provider} from 'react-redux';
const history = createBrowserHistory();
const rstore = createStore( // (1) Storeの生成
combineReducers(
{
apps: appReducer,
router: routerReducer,
}
),
applyMiddleware(
routerMiddleware(history)
)
)
function renderApp() {
ReactDOM.render(
<Provider store={rstore}>
<ConnectedRouter history={history}> // (2)ConnectedRouterの適用
<div>
<ul>
<li><Link to='/'>App</Link></li>
<li><Link to='/second'>Second</Link></li>
</ul>
<hr/>
<Route exact path='/' component={App} />
<Route exact path='/second' component={Second} />
</div>
</ConnectedRouter>
</Provider>,
document.getElementById('root')
);
}
renderApp();
ここまでの説明のように,(1)でStoreを生成し,(2)でConnectedRouter
を適用している.
ラッパーの修正
複数のReducerを合成したことにより.Storeの構成が変更された.そのため,mapStoreToProps
でStoreから値を取り出してPropsに設定する処理を変更する必要がある.先の説明の通り.全体のStoreは1つで変わらないが,combineReducer
がそれぞれのReducerで管理するStoreを,名前をつけて合成したからである.
combineReducers(
{
apps: appReducer,
router: routerReducer,
}
)
上記のように,今回アプリケーションで用意するappReducer
には,"apps"という名前がついているので,これまでtasks
やtask
としてアクセスしていた値に,apps
を追加する必要がある.具体的には,"container"に配置したItem3.js
, ItemList.js
を修正する.
(省略)
function mapStateToProps({apps}) { (1) appsを受け取る
const {task, tasks} = apps;
return ({
task,
tasks
});
}
(省略)
(1)
共通のStoreからapps
(と名前のつけられたappReducerが管理するStoreオブジェクト)を取り出し.appsに代入している.
function mapStateToProps({apps}) { // (1) appsを受け取る
const {tasks} = apps;
return ({
items: tasks
});
}
(1)
Item3.js
と同様.
ここまでの修正を加えることにより.React-Router-Reduxを導入してこれまでと同じ動作をするアプリケーションが実行できる.
ここまでの全ソースコードを示す.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import appReducer from './reducers/items';
import App from './components/App';
import Second from './components/Second';
import {BrowserRouter, HashRouter, Route, Link} from 'react-router-dom';
import { createStore,
combineReducers,
applyMiddleware
} from 'redux';
import {routerReducer, routerMiddleware } from 'react-router-redux';
import {ConnectedRouter} from 'react-router-redux';
import createBrowserHistory from 'history/createBrowserHistory';
import {Provider} from 'react-redux';
const store = createStore(appReducer);
const history = createBrowserHistory();
const rstore = createStore(
combineReducers(
{
apps: appReducer,
router: routerReducer,
}
),
applyMiddleware(
routerMiddleware(history)
)
)
function renderApp() {
ReactDOM.render(
<Provider store={rstore}>
<ConnectedRouter history={history}>
<div>
<ul>
<li><Link to='/'>App</Link></li>
<li><Link to='/second'>Second</Link></li>
</ul>
<hr/>
<Route exact path='/' component={App} />
<Route exact path='/second' component={Second} />
</div>
</ConnectedRouter>
</Provider>,
document.getElementById('root')
);
}
renderApp();
const initialState = {
task: '',
tasks: [],
};
function appReducer(state = initialState, action) {
switch(action.type) {
case 'INPUT_TASK':
return {
...state,
task: action.payload.task,
};
case 'ADD_TASK':
const nl = state.tasks.concat([action.payload.task]);
return {
...state,
tasks: nl
};
default:
return state;
}
}
export default appReducer;
export const inputTask = (task) => {
return ({
type: 'INPUT_TASK',
payload: {
task
}
});
}
export const addTask = (task) => {
return ({
type: 'ADD_TASK',
payload: {
task
}
});
}
import {connect} from 'react-redux';
import Input3 from '../components/Input3';
import {inputTask, addTask} from '../actions/items';
function mapStateToProps({apps}) {
console.log("mapStateToProps in input3");
console.log(apps);
const {task, tasks} = apps;
return ({
task,
tasks
});
}
function mapDispatchToProps(dispatch) {
return ({
inputTask(task) {
dispatch(inputTask(task));
},
addTask(task) {
dispatch(addTask(task));
},
});
}
export default connect(mapStateToProps, mapDispatchToProps)(Input3);
import {connect} from 'react-redux';
import ItemList from '../components/ItemList';
function mapStateToProps({apps}) {
const {tasks} = apps;
return ({
items: tasks
});
}
export default connect(mapStateToProps)(ItemList);
import React, { Component } from 'react';
import './App.css';
import Input3 from '../containers/Input3';
import ItemList from '../containers/ItemList';
class App extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<h1>Todo App</h1>
<Input3/>
<ul>
<ItemList />
</ul>
</div>
);
}
}
export default App;
import React, {Component} from 'react';
class Input3 extends Component {
constructor(props) {
super(props);
this.addItem = this.addItem.bind(this);
}
addItem() {
this.props.addTask(this.props.task);
}
render() {
return (
<div>
<input type="text" onChange={(e) => {this.props.inputTask(e.target.value)}} />
<button onClick={this.addItem}>Add</button><br/>
</div>
)
}
}
export default Input3;
import React from 'react'
const Item = (props) => {
return (
<li>{props.name}</li>
)
}
export default Item;
import React from 'react';
import Item from './Item';
const ItemList = ({items}) => {
let list = [];
for (var i = 0; i < items.length; ++i) {
list.push(<Item key = {i} name={items[i]} />);
}
return list;
}
export default ItemList;
import React from 'react';
export default (props) => {
return (
<h2>Hello Second</h2>
);
}
プログラムでルーティングを制御
routerMiddleware
を適用したことにより,プログラムでルーティングが制御できるようになる.そこで,Item3.jsに1つボタンを追加し,"ボタンを押したらSecondにルーティングする",ということをやってみる.
具体的にやることは,
- components/Input3.jsにボタンを追加
- 追加したボタンのイベントハンドラでProps経由で渡される関数を実行
- containers/Input3.jsの
mapDispatchToProps
で,2にわたす関数を追加. - 3で渡す関数の中で,
dispatch(push("second"))
を実行
のようにする.
手順4でしたdispatch(push("/second"))
を見ればわかるように,プログラムでルーティングを制御するには,dispatch
を使って変更をReducerに伝える.その際,push
を使ってhistoryを変更する.historyに渡す文字列が,これまで<Link to>
を使って渡していた文字列となる.
関連するファイルだけ示す.上記の全ソースコードから当該ファイルを入れ替えれば動く(はず).
import React, {Component} from 'react';
class Input3 extends Component {
constructor(props) {
super(props);
this.addItem = this.addItem.bind(this);
}
addItem() {
this.props.addTask(this.props.task);
}
render() {
return (
<div>
<input type="text" onChange={(e) => {this.props.inputTask(e.target.value)}} />
<button onClick={this.addItem}>Add</button><br/>
<button onClick={(e) => {this.props.gotoSecond()}}>Second</button> // 追加(手順1と2)
</div>
)
}
}
export default Input3;
import {connect} from 'react-redux';
import Input3 from '../components/Input3';
import {inputTask, addTask} from '../actions/items';
import { push } from 'react-router-redux'; // pushのインポート
function mapStateToProps({apps}) {
const {task, tasks} = apps;
return ({
task,
tasks
});
}
function mapDispatchToProps(dispatch) {
return ({
inputTask(task) {
dispatch(inputTask(task));
},
addTask(task) {
dispatch(addTask(task));
},
gotoSecond() { // 追加(手順3)
dispatch(push("/second")); // 追加(手順4)
},
});
}
export default connect(mapStateToProps, mapDispatchToProps)(Input3);
以上,React-Routerを利用したルーティングとReact-Router-Reduxを利用したルーティングを簡単に(?)書いてみた.