React Redux router saga material-ui 和当前路由对应的tabs
React Redux router saga material-ui and tabs corresponding to current route
我对反应很陌生,我正在使用 react-boilerplate + material-ui
我有这样的标签:
而且我希望能够更改当前选项卡,以便它会更改当前路线,反之亦然。
此外,当使用路由刷新页面时,它应该转到正确的选项卡。
所以我有这样的 tabpagechooser 容器组件:
index.js:
/*
*
* TabsPageChooser
*
*/
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { createStructuredSelector } from 'reselect';
import { changeTab } from './actions';
import makeSelectTab from './selectors';
import messages from './messages';
import {Tabs, Tab} from 'material-ui/Tabs';
import FontIcon from 'material-ui/FontIcon';
export class TabsPageChooser extends React.Component { // eslint-disable-line react/prefer-stateless-function
constructor(props) {
super(props)
this.handleHome = this.props.onChangeTab.bind(null, 0);
this.handleSettings = this.props.onChangeTab.bind(null, 1);
this.handleAbout = this.props.onChangeTab.bind(null, 2);
}
render() {
console.log(this.props);
return (
<Tabs initialSelectedIndex={this.props.tab.tabIdx} >
<Tab
icon={<FontIcon className="material-icons">home</FontIcon>}
label={<FormattedMessage {...messages.home} />}
onActive={this.handleHome} />
<Tab
icon={<FontIcon className="material-icons">settings</FontIcon>}
label={<FormattedMessage {...messages.settings} />}
onActive={this.handleSettings} />
<Tab
icon={<FontIcon className="material-icons">favorite</FontIcon>}
label={<FormattedMessage {...messages.about} />}
onActive={this.handleAbout} />
</Tabs>
);
}
}
TabsPageChooser.propTypes = {
onChangeTab: React.PropTypes.func,
};
const mapStateToProps = createStructuredSelector({
tab: makeSelectTab(),
});
function mapDispatchToProps(dispatch) {
return {
onChangeTab: (tabId) => {
dispatch(changeTab(tabId));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(TabsPageChooser);
actions.js:
/*
*
* TabsPageChooser actions
*
*/
import {
ROUTES_ID,
CHANGE_TAB,
} from './constants';
export function changeTab(tabId) {
return {
type: CHANGE_TAB,
tab: tabId,
};
}
export function urlFromId(tabId) {
if (!(tabId > 0 && tabId < ROUTES_ID)) {
return '/';
}
return ROUTES_ID[tabId];
}
export function changeTabFromUrl(url) {
console.log(url);
return changeTab(ROUTES_ID.indexOf(url));
}
constants.js:
/*
*
* TabsPageChooser constants
*
*/
export const CHANGE_TAB = 'app/TabsPageChooser/CHANGE_TAB';
export const ROUTES_ID = [
'/',
'/settings',
'/about',
];
reducer.js:
/*
*
* TabsPageChooser reducer
*
*/
import { fromJS } from 'immutable';
import {
CHANGE_TAB,
} from './constants';
const initialState = fromJS({
tabIdx: 0,
});
function tabsPageChooserReducer(state = initialState, action) {
switch (action.type) {
case CHANGE_TAB:
return state.set('tabIdx', action.tab);
default:
return state;
}
}
export default tabsPageChooserReducer;
sagas.js:
import { take, call, put, select, takeLatest, takeEvery } from 'redux-saga/effects';
import { push } from 'react-router-redux';
import { changeTabFromUrl, urlFromId } from 'containers/TabsPageChooser/actions';
import { makeSelectTab } from 'containers/TabsPageChooser/selectors';
import { CHANGE_TAB } from 'containers/TabsPageChooser/constants';
import { LOCATION_CHANGE } from 'react-router-redux';
function* doChangeTab(action) {
//Act as dispatch()
yield put(changeTabFromUrl(action.payload.pathname));
}
function* doChangeUrl(action) {
//Act as dispatch()
yield put(push(urlFromId(action.tab.tabId)));
}
// Individual exports for testing
export function* defaultSagas() {
yield takeEvery(LOCATION_CHANGE, doChangeTab);
yield takeEvery(CHANGE_TAB, doChangeUrl);
}
// All sagas to be loaded
export default [
defaultSagas,
];
我的问题尤其是最后一个文件,LOCATION_CHANGE 事件触发了 changeTab 操作,该操作又触发了 CHANGE_TAB 事件,该事件触发了位置更改等...,
我做错了什么,我该怎么办?
我终于成功了,
我改变了什么:
/*
*
* TabsChooser
*
*/
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { createStructuredSelector } from 'reselect';
import { changeTab } from 'containers/App/actions';
import { makeSelectLocationState, makeSelectTabsChooser } from 'containers/App/selectors';
import messages from './messages';
import {Tabs, Tab} from 'material-ui/Tabs';
import FontIcon from 'material-ui/FontIcon';
const locationId = [
'/',
'/settings',
'/about',
];
export class TabsChooser extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
this.contentsTab = [
{ route: this.props.onChangeTab.bind(null, locationId[0]), icon: <FontIcon className='material-icons'>home</FontIcon>, label: <FormattedMessage {...messages.home} />, },
{ route: this.props.onChangeTab.bind(null, locationId[1]), icon: <FontIcon className='material-icons'>settings</FontIcon>, label: <FormattedMessage {...messages.settings} />, },
{ route: this.props.onChangeTab.bind(null, locationId[2]), icon: <FontIcon className='material-icons'>favorite</FontIcon>, label: <FormattedMessage {...messages.about} />, },
];
let tabId = locationId.indexOf(this.props.tabLocation);
return (
<div>
<Tabs value={tabId} >
{this.contentsTab.map((tab, i) =>
<Tab key={i} value={i} icon={tab.icon} label={tab.label} onActive={tab.route} />
)}
</Tabs>
</div>
);
}
}
TabsChooser.propTypes = {
onChangeTab: React.PropTypes.func,
tabLocation: React.PropTypes.string,
};
function mapDispatchToProps(dispatch) {
return {
onChangeTab: (location) => dispatch(changeTab(location)),
};
}
const mapStateToProps = createStructuredSelector({
tabLocation: makeSelectTabsChooser(),
});
export default connect(mapStateToProps, mapDispatchToProps)(TabsChooser);
我现在在 changeTab() 中发送位置而不是选项卡 ID,
我将 action.js、reducer.js、selector.js 和 sagas.js 移动到 containers/App
action.js:
/*
* App Actions
*
*/
import { CHANGE_TAB, TABCHANGE_LOCATION } from './constants'
export function changeTab(tabLocation) {
return {
type: CHANGE_TAB,
tabLocation,
};
}
export function changeLocation(tabLocation) {
return {
type: TABCHANGE_LOCATION,
tabLocation,
};
}
constants.js:
/*
* AppConstants
*/
export const CHANGE_TAB = 'app/App/CHANGE_TAB';
export const TABCHANGE_LOCATION = 'app/App/TABCHANGE_LOCATION';
reducer.js:
/*
* AppReducer
*
*/
import { fromJS } from 'immutable';
import {
CHANGE_TAB,
TABCHANGE_LOCATION,
} from './constants';
// The initial state of the App
const initialState = fromJS({
tabLocation: window.location.pathname // Initial location from uri
});
function appReducer(state = initialState, action) {
switch (action.type) {
case CHANGE_TAB:
return state.set('tabLocation', action.tabLocation);
case TABCHANGE_LOCATION:
return state.set('tabLocation', action.tabLocation);
default:
return state;
}
}
export default appReducer;
initialState tabLocation 使用 window.location.pathname 设置,因此在应用程序启动时选择正确的选项卡。
selector.js:
/**
* The global state selectors
*/
import { createSelector } from 'reselect';
const selectGlobal = (state) => state.get('global');
const makeSelectLocationState = () => {
let prevRoutingState;
let prevRoutingStateJS;
return (state) => {
const routingState = state.get('route'); // or state.route
if (!routingState.equals(prevRoutingState)) {
prevRoutingState = routingState;
prevRoutingStateJS = routingState.toJS();
}
return prevRoutingStateJS;
};
};
const makeSelectTabsChooser = () => createSelector(
selectGlobal,
(globalState) => globalState.getIn(['tabLocation'])
);
export {
selectGlobal,
makeSelectLocationState,
makeSelectTabsChooser,
};
sagas.js:
import { take, call, put, select, takeLatest, takeEvery, cancel } from 'redux-saga/effects';
import { push } from 'react-router-redux';
import { changeLocation } from './actions';
import { makeSelectTabsChooser } from './selectors';
import { CHANGE_TAB } from './constants';
import { LOCATION_CHANGE } from 'react-router-redux';
function* updateLocation(action) {
//put() act as dispatch()
const url = yield put(push(action.tabLocation));
}
function* updateTab(action) {
const loc = yield put(changeLocation(action.payload.pathname));
}
// Individual exports for testing
export function* defaultSagas() {
const watcher = yield takeLatest(CHANGE_TAB, updateLocation);
const watcher2 = yield takeLatest(LOCATION_CHANGE, updateTab);
}
// All sagas to be loaded
export default [
defaultSagas,
];
所以传奇终于结束了。
我对反应很陌生,我正在使用 react-boilerplate + material-ui
我有这样的标签:
而且我希望能够更改当前选项卡,以便它会更改当前路线,反之亦然。 此外,当使用路由刷新页面时,它应该转到正确的选项卡。
所以我有这样的 tabpagechooser 容器组件:
index.js:
/*
*
* TabsPageChooser
*
*/
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { createStructuredSelector } from 'reselect';
import { changeTab } from './actions';
import makeSelectTab from './selectors';
import messages from './messages';
import {Tabs, Tab} from 'material-ui/Tabs';
import FontIcon from 'material-ui/FontIcon';
export class TabsPageChooser extends React.Component { // eslint-disable-line react/prefer-stateless-function
constructor(props) {
super(props)
this.handleHome = this.props.onChangeTab.bind(null, 0);
this.handleSettings = this.props.onChangeTab.bind(null, 1);
this.handleAbout = this.props.onChangeTab.bind(null, 2);
}
render() {
console.log(this.props);
return (
<Tabs initialSelectedIndex={this.props.tab.tabIdx} >
<Tab
icon={<FontIcon className="material-icons">home</FontIcon>}
label={<FormattedMessage {...messages.home} />}
onActive={this.handleHome} />
<Tab
icon={<FontIcon className="material-icons">settings</FontIcon>}
label={<FormattedMessage {...messages.settings} />}
onActive={this.handleSettings} />
<Tab
icon={<FontIcon className="material-icons">favorite</FontIcon>}
label={<FormattedMessage {...messages.about} />}
onActive={this.handleAbout} />
</Tabs>
);
}
}
TabsPageChooser.propTypes = {
onChangeTab: React.PropTypes.func,
};
const mapStateToProps = createStructuredSelector({
tab: makeSelectTab(),
});
function mapDispatchToProps(dispatch) {
return {
onChangeTab: (tabId) => {
dispatch(changeTab(tabId));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(TabsPageChooser);
actions.js:
/*
*
* TabsPageChooser actions
*
*/
import {
ROUTES_ID,
CHANGE_TAB,
} from './constants';
export function changeTab(tabId) {
return {
type: CHANGE_TAB,
tab: tabId,
};
}
export function urlFromId(tabId) {
if (!(tabId > 0 && tabId < ROUTES_ID)) {
return '/';
}
return ROUTES_ID[tabId];
}
export function changeTabFromUrl(url) {
console.log(url);
return changeTab(ROUTES_ID.indexOf(url));
}
constants.js:
/*
*
* TabsPageChooser constants
*
*/
export const CHANGE_TAB = 'app/TabsPageChooser/CHANGE_TAB';
export const ROUTES_ID = [
'/',
'/settings',
'/about',
];
reducer.js:
/*
*
* TabsPageChooser reducer
*
*/
import { fromJS } from 'immutable';
import {
CHANGE_TAB,
} from './constants';
const initialState = fromJS({
tabIdx: 0,
});
function tabsPageChooserReducer(state = initialState, action) {
switch (action.type) {
case CHANGE_TAB:
return state.set('tabIdx', action.tab);
default:
return state;
}
}
export default tabsPageChooserReducer;
sagas.js:
import { take, call, put, select, takeLatest, takeEvery } from 'redux-saga/effects';
import { push } from 'react-router-redux';
import { changeTabFromUrl, urlFromId } from 'containers/TabsPageChooser/actions';
import { makeSelectTab } from 'containers/TabsPageChooser/selectors';
import { CHANGE_TAB } from 'containers/TabsPageChooser/constants';
import { LOCATION_CHANGE } from 'react-router-redux';
function* doChangeTab(action) {
//Act as dispatch()
yield put(changeTabFromUrl(action.payload.pathname));
}
function* doChangeUrl(action) {
//Act as dispatch()
yield put(push(urlFromId(action.tab.tabId)));
}
// Individual exports for testing
export function* defaultSagas() {
yield takeEvery(LOCATION_CHANGE, doChangeTab);
yield takeEvery(CHANGE_TAB, doChangeUrl);
}
// All sagas to be loaded
export default [
defaultSagas,
];
我的问题尤其是最后一个文件,LOCATION_CHANGE 事件触发了 changeTab 操作,该操作又触发了 CHANGE_TAB 事件,该事件触发了位置更改等...,
我做错了什么,我该怎么办?
我终于成功了, 我改变了什么:
/*
*
* TabsChooser
*
*/
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { createStructuredSelector } from 'reselect';
import { changeTab } from 'containers/App/actions';
import { makeSelectLocationState, makeSelectTabsChooser } from 'containers/App/selectors';
import messages from './messages';
import {Tabs, Tab} from 'material-ui/Tabs';
import FontIcon from 'material-ui/FontIcon';
const locationId = [
'/',
'/settings',
'/about',
];
export class TabsChooser extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
this.contentsTab = [
{ route: this.props.onChangeTab.bind(null, locationId[0]), icon: <FontIcon className='material-icons'>home</FontIcon>, label: <FormattedMessage {...messages.home} />, },
{ route: this.props.onChangeTab.bind(null, locationId[1]), icon: <FontIcon className='material-icons'>settings</FontIcon>, label: <FormattedMessage {...messages.settings} />, },
{ route: this.props.onChangeTab.bind(null, locationId[2]), icon: <FontIcon className='material-icons'>favorite</FontIcon>, label: <FormattedMessage {...messages.about} />, },
];
let tabId = locationId.indexOf(this.props.tabLocation);
return (
<div>
<Tabs value={tabId} >
{this.contentsTab.map((tab, i) =>
<Tab key={i} value={i} icon={tab.icon} label={tab.label} onActive={tab.route} />
)}
</Tabs>
</div>
);
}
}
TabsChooser.propTypes = {
onChangeTab: React.PropTypes.func,
tabLocation: React.PropTypes.string,
};
function mapDispatchToProps(dispatch) {
return {
onChangeTab: (location) => dispatch(changeTab(location)),
};
}
const mapStateToProps = createStructuredSelector({
tabLocation: makeSelectTabsChooser(),
});
export default connect(mapStateToProps, mapDispatchToProps)(TabsChooser);
我现在在 changeTab() 中发送位置而不是选项卡 ID, 我将 action.js、reducer.js、selector.js 和 sagas.js 移动到 containers/App
action.js:
/*
* App Actions
*
*/
import { CHANGE_TAB, TABCHANGE_LOCATION } from './constants'
export function changeTab(tabLocation) {
return {
type: CHANGE_TAB,
tabLocation,
};
}
export function changeLocation(tabLocation) {
return {
type: TABCHANGE_LOCATION,
tabLocation,
};
}
constants.js:
/*
* AppConstants
*/
export const CHANGE_TAB = 'app/App/CHANGE_TAB';
export const TABCHANGE_LOCATION = 'app/App/TABCHANGE_LOCATION';
reducer.js:
/*
* AppReducer
*
*/
import { fromJS } from 'immutable';
import {
CHANGE_TAB,
TABCHANGE_LOCATION,
} from './constants';
// The initial state of the App
const initialState = fromJS({
tabLocation: window.location.pathname // Initial location from uri
});
function appReducer(state = initialState, action) {
switch (action.type) {
case CHANGE_TAB:
return state.set('tabLocation', action.tabLocation);
case TABCHANGE_LOCATION:
return state.set('tabLocation', action.tabLocation);
default:
return state;
}
}
export default appReducer;
initialState tabLocation 使用 window.location.pathname 设置,因此在应用程序启动时选择正确的选项卡。
selector.js:
/**
* The global state selectors
*/
import { createSelector } from 'reselect';
const selectGlobal = (state) => state.get('global');
const makeSelectLocationState = () => {
let prevRoutingState;
let prevRoutingStateJS;
return (state) => {
const routingState = state.get('route'); // or state.route
if (!routingState.equals(prevRoutingState)) {
prevRoutingState = routingState;
prevRoutingStateJS = routingState.toJS();
}
return prevRoutingStateJS;
};
};
const makeSelectTabsChooser = () => createSelector(
selectGlobal,
(globalState) => globalState.getIn(['tabLocation'])
);
export {
selectGlobal,
makeSelectLocationState,
makeSelectTabsChooser,
};
sagas.js:
import { take, call, put, select, takeLatest, takeEvery, cancel } from 'redux-saga/effects';
import { push } from 'react-router-redux';
import { changeLocation } from './actions';
import { makeSelectTabsChooser } from './selectors';
import { CHANGE_TAB } from './constants';
import { LOCATION_CHANGE } from 'react-router-redux';
function* updateLocation(action) {
//put() act as dispatch()
const url = yield put(push(action.tabLocation));
}
function* updateTab(action) {
const loc = yield put(changeLocation(action.payload.pathname));
}
// Individual exports for testing
export function* defaultSagas() {
const watcher = yield takeLatest(CHANGE_TAB, updateLocation);
const watcher2 = yield takeLatest(LOCATION_CHANGE, updateTab);
}
// All sagas to be loaded
export default [
defaultSagas,
];
所以传奇终于结束了。