如果有人感兴趣,这里是一个带有 redux 的动态反应路由器。
[世界上有太多的待办事项示例,如果我不是总是想出真实世界示例的人,那就太好了。]
index.js
import 'babel-core/polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import Routes from './router/routes';
// redux
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
// styles
import './index.css';
// Setting up entire state 'schema' at inception
const store = configureStore();
ReactDOM.render(
<Provider store={ store }>
<Routes />
</Provider>,
document.getElementById('root')
);
路由.js
import React, { Component, PropTypes } from 'react';
import { Router, Route, IndexRoute } from 'react-router';
// redux
import { connect } from 'react-redux';
import { fetchNavItemsIfNeeded } from '../actions/nav-items-actions';
// history
import createBrowserHistory from 'history/lib/createBrowserHistory';
const history = createBrowserHistory();
import App from '../containers/app/App';
import Home from '../containers/home/Home';
import NotFound from '../containers/misc/NotFound';
class Routes extends Component {
constructor() {
super();
this.state = {
routes: []
};
}
fetchMenuSystem(data) {
const self = this;
const currRoutesState = this.state.routes;
const routes = data === undefined ? this.props.navItems : data;
routes.map((route) => {
// set paths up first
let currPaths = [];
if (route.paths !== undefined) {
currPaths = route.paths;
} else {
currPaths.push(route.linkTo);
}
// Components - first check for ecomMods
let currComponent;
if (route.ecomMod !== undefined) {
currComponent = require('../containers/' + route.ecomMod);
// clear out currPath if this is an ecom Module
// and start a new currPaths array
currPaths = [];
if (route.parentId === null) {
currPaths.push(route.ecomMod);
} else {
currPaths.push(route.ecomMod + '/:id');
}
} else {
currComponent = require('../containers/' + route.component);
}
currPaths.map((currPath, idx) => {
const props = { key: idx, path: currPath, component: currComponent };
currRoutesState.push(<Route { ...props } />);
});
if (route.childNodes !== undefined) {
self.fetchMenuSystem(route.childNodes);
}
});
return currRoutesState;
}
componentDidMount() {
const { dispatch } = this.props;
const clientId = '7B3E7eWWPizd11n';
dispatch(fetchNavItemsIfNeeded(clientId));
}
render() {
if (!this.props.navItems) return <div>Loading ...</div>;
return (
<Router history={ history }>
<Route path="/" component={ App }>
<IndexRoute component={ Home }/>
{ this.fetchMenuSystem() }
<Route path="*" component={ NotFound }/>
</Route>
</Router>
);
}
}
function mapStateToProps(state) {
const { navItemsPerClient } = state;
if (!navItemsPerClient) {
return {
isFetching: false,
didInvalidate: false,
navItems: [],
error: null
};
}
return {
error: navItemsPerClient.error,
isFetching: navItemsPerClient.isFetching,
didInvalidate: navItemsPerClient.didInvalidate,
navItems: navItemsPerClient.navItems
};
}
Routes.propTypes = {
dispatch: PropTypes.func.isRequired,
navItems: PropTypes.array
};
export default connect(mapStateToProps)(Routes);
导航项目-actions.js
import 'isomorphic-fetch';
import { checkStatus, parseJSON } from './utils';
export const INVALIDATE_NAV_ITEMS = 'INVALIDATE_NAV_ITEMS';
export const NAV_ITEMS_REQUEST = 'NAV_ITEMS_REQUEST';
export const NAV_ITEMS_SUCCESS = 'NAV_ITEMS_SUCCESS';
export const NAV_ITEMS_FAILURE = 'NAV_ITEMS_FAILURE';
export function invalidateNavItems() {
return {
type: INVALIDATE_NAV_ITEMS
};
}
function navItemsRequest() {
return {
type: NAV_ITEMS_REQUEST
};
}
function navItemsSuccess(payload) {
return {
type: NAV_ITEMS_SUCCESS,
navItems: payload.navItems
};
}
function navItemsFailure(error) {
return {
type: NAV_ITEMS_FAILURE,
error
};
}
export function fetchNavItems(clientId) {
const API_URL = (`../data/${clientId}/navigation/navigation.json`);
return dispatch => {
dispatch(navItemsRequest());
return fetch(API_URL)
.then(checkStatus)
.then(parseJSON)
.then(json => dispatch(navItemsSuccess(json)))
.catch(function(error) {
const response = error.response;
if (response === undefined) {
dispatch(navItemsFailure(error));
} else {
parseJSON(response)
.then(function(json) {
error.status = response.status;
error.statusText = response.statusText;
error.message = json.message;
dispatch(navItemsFailure(error));
});
}
});
};
}
function shouldFetchNavItems(state) {
// Check cache first
const navItems = state.navItemsPerClient;
if (!navItems || navItems.length === undefined) {
// Not cached, should fetch
return true;
}
if (navItems.isFetching) {
// Shouldn't fetch since fetching is running
return false;
}
// Should fetch if cache was invalidate
return navItems.didInvalidate;
}
export function fetchNavItemsIfNeeded(clientId) {
return (dispatch, getState) => {
if (shouldFetchNavItems(getState())) {
return dispatch(fetchNavItems(clientId));
}
};
}
实用程序.js
export function checkStatus(response) {
if (!response.ok) { // (response.status < 200 || response.status > 300)
const error = new Error(response.statusText);
error.response = response;
throw error;
}
return response;
}
export function parseJSON(response) {
return response.json();
}
导航项目-reducer.js
import {
INVALIDATE_NAV_ITEMS, NAV_ITEMS_REQUEST,
NAV_ITEMS_SUCCESS, NAV_ITEMS_FAILURE
} from '../actions/nav-items-actions';
function navItems(state = {
isFetching: false,
didInvalidate: false,
navItems: [],
error: null
}, action) {
switch (action.type) {
case INVALIDATE_NAV_ITEMS:
return Object.assign({}, state, {
didInvalidate: true
});
case NAV_ITEMS_REQUEST:
return Object.assign({}, state, {
isFetching: true,
didInvalidate: false
});
case NAV_ITEMS_SUCCESS:
return Object.assign({}, state, {
isFetching: false,
didInvalidate: false,
navItems: action.navItems,
error: null
});
case NAV_ITEMS_FAILURE:
return Object.assign({}, state, {
isFetching: false,
didInvalidate: false,
error: action.error
});
default:
return state;
}
}
export function navItemsPerClient(state = { }, action) {
switch (action.type) {
case INVALIDATE_NAV_ITEMS:
case NAV_ITEMS_REQUEST:
case NAV_ITEMS_SUCCESS:
case NAV_ITEMS_FAILURE:
return navItems(state, action);
default:
return state;
}
}
配置-store.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import auth from '../reducers/auth-reducer';
import { navItemsPerClient } from '../reducers/nav-items-reducer';
const logger = createLogger();
const reducer = combineReducers(
{
auth,
navItemsPerClient
}
);
const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware,
logger
)(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(reducer, initialState);
}
导航.json
{
"incomplete_results": false,
"navItems": [
{
"linkTo": "/about",
"component": "about/About",
"childNodes": [
{
"linkTo": "/proforma",
"component": "about/ProForma"
}
]
},
{
"linkTo": "/login",
"component": "login/Login"
},
{
"linkTo": "/none",
"component": "misc/RestrictPage",
"childNodes": [
{
"linkTo": "/users",
"component": "user/UsersPage"
},
{
"linkTo": "/repos",
"component": "repo/ReposPage"
}
]
}
]
}
包.json
"dependencies": {
"body-parser": "^1.14.1",
"classnames": "^2.2.0",
"express": "^4.13.3",
"fixed-data-table": "^0.5.0",
"history": "^1.13.0",
"isomorphic-fetch": "^2.1.1",
"lodash": "^3.10.1",
"react": "^0.14.3",
"react-dom": "^0.14.3",
"react-redux": "^4.0.0",
"react-router": "^1.0.2",
"redux": "^3.0.4",
"redux-logger": "^2.0.4",
"redux-thunk": "^1.0.0"
},
"devDependencies": {
"babel": "^5.8.29",
"babel-core": "^5.8.33",
"babel-eslint": "^4.1.5",
"babel-loader": "^5.3.2",
"eslint": "^1.9.0",
"eslint-config-airbnb": "^1.0.0",
"eslint-loader": "^1.1.1",
"eslint-plugin-react": "^3.8.0",
"file-loader": "^0.8.4",
"raw-loader": "^0.5.1",
"redbox-react": "^1.1.1",
"rimraf": "^2.4.3",
"stats-webpack-plugin": "^0.2.2",
"style-loader": "^0.13.0",
"url-loader": "^0.5.6",
"webpack": "^1.12.4",
"webpack-dev-middleware": "^1.2.0",
"webpack-hot-middleware": "^2.4.1"
}