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
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
※かつてよく使われていたreact-router-reduxは現在はメンテされておらず、connected-react-routerに置き換えられています。
Example
以下の過去記事で紹介したものをExampleとします。
http://yahoo-shopping.s3-website-ap-northeast-1.amazonaws.com/
Material-UI レスポンシブデザイン- AppBar & Drawer
react-router-domのBrowserRouterを、connected-react-routerのConnectedRouterで置き換えます。
historyはJavaScriptライブラリで、react-routerがバックグランドで利用しているものです。connected-react-routerはhistory methodを使いやすくしてくれます。 ==> ReactTraining/history
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 ミドルウェアも組み込みます。
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のコードは無視してください。
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を変更できることに注意してください。
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);
*****/
}
}
今回は以上です。