LoginSignup
6
5

More than 3 years have passed since last update.

React-Redux超入門(5)

Last updated at Posted at 2018-08-16

概要

前回までのアプリケーションにルーティングを追加する.ルーティングとは,いわゆる画面遷移とほぼ等価で.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ではこの他にもコンポーネントを提供しているが,ここでは割愛.

次に,これまでのアプリケーションに切り替えて表示するコンポーネントを追加する.

/components/Second.js
import React from 'react';

export default (props) => {
    return (
        <h2>Hello Second</h2>
    );
}

ルーティングの確認がしたいだけなので,これで十分.

BrowserRouter/HashRouterを用いたルーティングの実装

BrowserRouterを使ったルーティング

ルーティングはルートであるindex.jsに導入する.

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が表示されるようになる.なお,Routeexact属性は,パスが完全一致した時のみルーティングされる,という意味である.exact=trueであるが,exactがbooleanなので,trueは省略可能.

HashRouterを使ったルーティング

HashRouterを使った場合は以下のようになる.

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} 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を利用したルーティング

ここまでの方法では,ルーティングするために

  1. Routeによるpathとコンポーネントのペアを定義
  2. Linkによってリンクを作成

してルーティングを実現してきた.Linkto属性に指定するのは特定の文字列であるため,実行結果に応じてルーティング先を切り替える,つまりプログラムで実行時にルーティングを切り替えるようなことができない.そこで,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.jsappReducerを定義しているので,両者を合成する必要がある.
次に,(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つのステートにマージした,ということである.大事なところ(だと思う)ので,もう一度何でこんなことをする必要があるのか,ということを考えると,

  1. Reducerの役割はStateとActionを受け取って新しいStateオブジェクトを返すことである
  2. Reduxでは通常アプリケーション全体で1つのStateを扱う.そのため,原則として1つのReducerに対して1つのStateがあるべきである.
  3. 複数Reducerが存在する場合,複数のStateが存在することになってしまい都合が悪い
  4. よって,combineReducerでそれぞれのReducerで管理するStateに名前をつけ,更にそれをまとめて1つのStateとすることで, 1つしかStateオブジェクトは存在しないというReduxの原則を守りつつ.各Reducerは自身が管理するStateしか知らなくてよい ということを実現する.

という手順で整理することで納得することができると思う.つまり,combineReducerをすることで,擬似的に複数のStateが存在するような感じになる.

ここ

ConnectedRouterの適用

これまで利用していたBrowserRouterHashRouterの代わりに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全体を示す.

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"という名前がついているので,これまでtaskstaskとしてアクセスしていた値に,appsを追加する必要がある.具体的には,"container"に配置したItem3.js, ItemList.jsを修正する.

container/Item3.js
(省略)
function mapStateToProps({apps}) { (1) appsを受け取る
    const {task, tasks} = apps;   
    return ({
        task,
        tasks
    });
}
(省略)

(1)
共通のStoreからapps(と名前のつけられたappReducerが管理するStoreオブジェクト)を取り出し.appsに代入している.

container/ItemList.js
function mapStateToProps({apps}) { // (1) appsを受け取る
    const {tasks} = apps;
    return ({
        items: tasks
    });
}

(1)
Item3.jsと同様.

ここまでの修正を加えることにより.React-Router-Reduxを導入してこれまでと同じ動作をするアプリケーションが実行できる.

ここまでの全ソースコードを示す.

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 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();
reducers/items.js
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;
actions/items.js

export const inputTask = (task) => {
    return ({
        type: 'INPUT_TASK',
        payload: {
            task
        }
        });
}

export const addTask = (task) => {
    return ({
        type: 'ADD_TASK',
        payload: {
            task
        }
    });
}
containers/Item3.js
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);

containers/ItemList.js
import {connect} from 'react-redux';
import ItemList from '../components/ItemList';


function mapStateToProps({apps}) {
    const {tasks} = apps;
    return ({
        items: tasks
    });
}
export default connect(mapStateToProps)(ItemList);
components/App.js
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;
components/Input3.js
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;
components/Item.js
import React from 'react'

const Item = (props) => {
    return (
        <li>{props.name}</li>
    )
}

export default Item;
components/ItemList.js
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;
components/Second.js
import React from 'react';

export default (props) => {
    return (
        <h2>Hello Second</h2>
    );
}

プログラムでルーティングを制御

routerMiddlewareを適用したことにより,プログラムでルーティングが制御できるようになる.そこで,Item3.jsに1つボタンを追加し,"ボタンを押したらSecondにルーティングする",ということをやってみる.
具体的にやることは,

  1. components/Input3.jsにボタンを追加
  2. 追加したボタンのイベントハンドラでProps経由で渡される関数を実行
  3. containers/Input3.jsのmapDispatchToPropsで,2にわたす関数を追加.
  4. 3で渡す関数の中で,dispatch(push("second"))を実行

のようにする.
手順4でしたdispatch(push("/second"))を見ればわかるように,プログラムでルーティングを制御するには,dispatchを使って変更をReducerに伝える.その際,pushを使ってhistoryを変更する.historyに渡す文字列が,これまで<Link to>を使って渡していた文字列となる.

関連するファイルだけ示す.上記の全ソースコードから当該ファイルを入れ替えれば動く(はず).

components/Input3.js
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;
containers/Input3.js
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を利用したルーティングを簡単に(?)書いてみた.

6
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5