react-router v4 と Redux

react-router で Redux を利用すれば、history methods (push, replace, go, goBack, goForward) を自由に使えるようになり便利です。そのためにconnected-react-routerをインストールして使います。その使い方を以下に説明します。


# connected-react-routerの概要

Webアプリ用にreact-router を使うには **react-router-dom** をインストールします。 ==> [REACT TRAINING / REACT ROUTER](https://reacttraining.com/react-router/web/guides/quick-start)


react-router のみではURLの変更を\<Link\>や\<Redirect\>でしか行えない。

**connected-react-routerを使えば、ルーティング情報をRedux storeで管理し、pushやreplace関数でURL変更を行うことができます。つまりredux-thunk や redux-saga において、history methods (push, replace, go, goBack, goForward)を含むaction を dispatch 売ることができます。** ==> [Connected React Router - github](https://github.com/supasate/connected-react-router)


※かつてよく使われていたreact-router-reduxは現在はメンテされておらず、connected-react-routerに置き換えられています。


# Example

以下の過去記事で紹介したものをExampleとします。

http://yahoo-shopping.s3-website-ap-northeast-1.amazonaws.com/
[Material-UI レスポンシブデザイン- AppBar & Drawer](https://qiita.com/sand/items/d4b6f39b5fe6027064b4)




react-router-domのBrowserRouterを、connected-react-routerのConnectedRouterで置き換えます。

historyはJavaScriptライブラリで、react-routerがバックグランドで利用しているものです。connected-react-routerはhistory methodを使いやすくしてくれます。 ==> [ReactTraining/history](https://github.com/ReactTraining/history)


```react:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router';
import createBrowserHistory from 'history/createBrowserHistory';
import App from './App';
import createStore from './createStore';

// connected-react-router - action経由でルーティングが可能、push,replace..
// historyを強化
const history = createBrowserHistory();
const store = createStore(history);

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <App />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
);
```

createStoreをconnected-react-routerを使うようにカスタマイズします。redux-thunk ミドルウェアも組み込みます。

```react:src/createStore
import { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import { routerMiddleware, connectRouter } from 'connected-react-router'

import * as reducers from './reducers';

// connected-react-router - action経由でルーティングが可能、push,replace..
// createStoreの再定義 - historyを引数で受け、connected-react-routerの利用を抽象化
export default function createStore(history) { 
  return reduxCreateStore( // オリジナル createStore の別名
    combineReducers({
      ...reducers,
      router: connectRouter(history),
    }),
    applyMiddleware(
      logger,
      thunk,
      routerMiddleware(history)
    )
  );
}
```

App.jsでRouteの定義を行っています。AppBarなどのMaterial-UIのコードは無視してください。

```react:src/App.js
import React from 'react';
import PropTypes from 'prop-types';
import AppBar from '@material-ui/core/AppBar';
import CssBaseline from '@material-ui/core/CssBaseline';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import { withStyles } from '@material-ui/core/styles';

import { Switch, Route, Redirect } from 'react-router-dom';
import Ranking from './containers/Ranking';
import Nav from './containers/Nav';
import { connect } from 'react-redux'
import { styles } from './Styles';


class ResponsiveDrawer extends React.Component {

  handleDrawerToggle = () => {
    this.props.dispatch({ type: 'MOBILE_TOGGLE' })
  };

  render() {
    const { classes, theme } = this.props;
    return (
      <div className={classes.root}>
        <CssBaseline />
        <AppBar position="fixed" className={classes.appBar}>
          <Toolbar>
            <IconButton
              color="inherit"
              aria-label="Open drawer"
              onClick={this.handleDrawerToggle}
              className={classes.menuButton}
            >
              <MenuIcon />
            </IconButton>
            <Typography variant="h6" color="inherit" noWrap>
              Yhoo!ショッピング
            </Typography>
          </Toolbar>
        </AppBar>
        <nav className={classes.drawer}>
          {/* The implementation can be swapped with js to avoid SEO duplication of links. */}
          <Nav />
        </nav>
        <main className={classes.content}>
          <div className={classes.toolbar} />
          <div>
            <Switch>
              <Route path="/all" component={Ranking} />
              <Route path="/category/1" 
                render={ () => <Redirect to="/all" /> }
              />
              <Route path="/category/:id" 
                render={ ({match}) => <Ranking categoryId={match.params.id} /> }
              />
            </Switch>
          </div>
        </main>
      </div>
    );
  }
}


ResponsiveDrawer.propTypes = {
  classes: PropTypes.object.isRequired,
  container: PropTypes.object,
  theme: PropTypes.object.isRequired,
};

export default connect() ( withStyles(styles, { withTheme: true })(ResponsiveDrawer) );
```

以下のようにredux-thunkのaction関数において、connected-react-routerから取り出したreplace や push で自由にhistoryにアクセスしURLを変更できることに注意してください。


```react:src/actions/Ranking.js
import fetchJsonp from 'fetch-jsonp';
import qs from 'qs';
// connected-react-router - action経由でルーティングが可能、push,replace..
import { replace, push } from 'connected-react-router';

const API_URL = 'https://shopping.yahooapis.jp/ShoppingWebService/V1/json/categoryRanking';

const APP_ID = 'xxxxxxxxxx';

const startRequest = category => ({
  type: 'START_REQUEST',
  payload: { category },
});


const receiveData = (category, error, response) => ({
  type: 'RECEIVE_DATA',
  payload: { category, error, response },
});


const finishRequest = category => ({
  type: 'FINISH_REQUEST',
  payload: { category },
});


// ** Redux-thunk
// ** action関数をラッピング - 内部で通常のactionを使いStoreを更新
export const fetchRanking = categoryId => {
  return async (dispatch, getState) => {
    const categories = getState().shopping.categories;
    const category = categories.find(category => (category.id === categoryId));
    if(typeof category === 'undefined') {
      dispatch(replace('/')); // URLをトップに変更
      return;
    }

    // ** 通常のaction
    dispatch(startRequest(category));

    const queryString = qs.stringify({
      appid: APP_ID,
      category_id: categoryId,
    });
console.log(queryString)

    try {
      const response = await fetchJsonp(`${API_URL}?${queryString}`);
      const data = await response.json();
      // ** 通常のaction
      dispatch(receiveData(category, null, data));
    } catch (err) {
      // ** 通常のaction
      dispatch(receiveData(category, err));
    }
    // ** 通常のaction
    dispatch(finishRequest(category));

    // test
    /*****
    setTimeout( () => {
      // dispatch(replace('/'));
      dispatch(push('/'));
    }, 3000);
    *****/
  }
}
```


今回は以上です。