Redux 和 React-Router:动态路由器不工作

Redux & React-Router: Dynamic Router Not Working

这在 Alt.js 上运行良好,然后我切换到 redux。我真的很难全神贯注于 redux。

错误:不变违规:对象作为 React 子项无效(已找到:具有键 {id、doc…}的对象}

我正在使用:

"react": "^0.14.3",
"react-dom": "^0.14.3",
"react-redux": "^4.0.0",
"react-router": "^1.0.2",
"redux": "^3.0.4"

任何人都可以看到为什么我收到错误并且没有显示任何组件吗?

import React, { Component, PropTypes } from 'react';
import Router, { Route } from 'react-router';
// redux
import { connect } from 'react-redux';
import { fetchNavItemsIfNeeded } from '../redux/actions/nav-item-actions';

class Routes extends Component {

  constructor(props) {
    super(props);
  }

  componentWillMount() {
    const { dispatch } = this.props;
    dispatch(fetchNavItemsIfNeeded('7B3E7eWWPizd11n'));
  }

  fetchMenuSystem(data) {
    const self = this;
    const currRoutesState = this.props.navItems;
    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('../components/eCommerce/' + (route.ecomMod).toLowerCase());
          // 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('../components/pages/');
        }

        currPaths.map((currPath) => {
          const props = { key: currPath, path: currPath, component: currComponent };
          currRoutesState.push(<Route { ...props } />);
        });

        if (route.childNodes !== undefined) {
          self.fetchMenuSystem(route.childNodes);
        }
      });
    return currRoutesState;
  }

  fetchRoutes() {
    const result = this.fetchMenuSystem();
    const clientId = this.props.clientId;
    return (
      <Route clientId={ clientId } component={ require('../components/APP') }>
        { result }
        <Route path="*" component={ require('../components/pages/Not-Found') }/>
      </Route>
    );
  }

  render() {
    if (!this.props.navItems) return <div>Loading ...</div>;
    const routerProps = {
      routes: this.fetchRoutes(),
      createElement: (component, props) => {
        return React.createElement(component, { ...props });
      }
    };
    return (
      <div>
        <Router { ...routerProps } history={ this.props.history }/>
      </div>
    );
  }
}

Routes.propTypes = {
  clientId: PropTypes.string.isRequired,
  dispatch: PropTypes.func.isRequired,
  error: PropTypes.object,
  history: PropTypes.object.isRequired,
  navItems: PropTypes.array.isRequired
};

function mapStateToProps(state) {
  const { navItemsPerClient } = state;
  if (!navItemsPerClient) {
    return {
      navItems: []
    };
  }
  return {
    navItems: navItemsPerClient.navItems
  };
}

export default connect(mapStateToProps)(Routes);

Alot of code I know, but the data is showing up, and it is really the { result } in this.fetchRoutes() that is giving me the problem. W/o it things work, but of course there goes 95% of the routes.

如果有人感兴趣,这里有一个 带有 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')
);

routes.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));
    }
  };
}

utils.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);
}

navigation.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"
        }
      ]
    }
  ]
}

package.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"
  }

react-router v1.0 及更高版本中,您需要执行以下操作:

import { Router, Route } from 'react-router';