Search code examples
reactjsreact-routerreduxredux-thunk

Redux & React-Router: Dynamic Router Not Working


This was working fine with Alt.js, then I switched to redux. I am really having a hard time wrapping my head around redux.

Error: Invariant Violation: Objects are not valid as a React child (found: object with keys {id, doc…}

enter image description here

I am using :

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

Can anyone see why I am getting an error and no component is showing up?

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.


Solution

  • If anyone is interested, here is a dynamic react-router with redux.
    [The world has too many todos examples, be nice if I wasn't the one always coming up with the real world examples.]

    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);
    

    nav-items-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();
    }
    

    nav-items-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;
      }
    }
    

    configure-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"
      }