2

I've got a sidebar with two buttons, 'test' and 'about'. Test (rocket icon) is rendered at '/test', and About (home icon) is rendered at '/'.

They're both located at the root of the app and are nested within a component.

When I start at '/' and click the Link to="/test" it always loads the 'About' component, and when I check the props for the componentDidMount of 'About', the match object contains match data for "/test".

Only when I refresh does it render the proper component, 'Test', again. Any idea why this is happening?

AppRoutes.js:

export class AppRoutes extends React.Component {

  render() {
    return (
      <div>
        <Switch>
          <Route
            exact path="/"
            render={(matchProps) => (
              <LazyLoad getComponent={() => import('pages/appPages/About')} {...matchProps} />
            )}
          />
          <Route
            path="/login"
            render={(matchProps) => (
              <LazyLoad getComponent={() => import('pages/appPages/Login')} {...matchProps} />
            )}
          />
          <Route
            path="/register"
            render={(matchProps) => (
              <LazyLoad getComponent={() => import('pages/appPages/Register')} {...matchProps} />
            )}
          />
          <Route
            path="/test"
            render={(matchProps) => (
              <LazyLoad getComponent={() => import('pages/appPages/Test')} {...matchProps} />
            )}
          />
...

AboutPage.js && TestPage.js (duplicates except for component name):

import React from 'react';

import SidebarContainer from 'containers/SidebarContainer';
import SidebarPageLayout from 'styles/SidebarPageLayout';

export const About = (props) => {
  console.log('About Loading: ', props);
  return (
    <SidebarPageLayout>
      <SidebarContainer />
      <div>About</div>
    </SidebarPageLayout>
  );
}

export default About;

SidebarContainer.js:

import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';

import Sidebar from 'sidebar/Sidebar';
import HamburgerButton from 'sidebar/HamburgerButton';
import AboutButton from 'sidebar/AboutButton';
import ProfileButton from 'sidebar/ProfileButton';
import TestButton from 'sidebar/TestButton';

export class SidebarContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      sidebarIsOpen: false,
      sidebarElements: [],
    };
  }

  componentDidMount() {
    if (!this.props.authenticated) {
      this.setState({
        sidebarElements: _.concat(this.state.sidebarElements, HamburgerButton, ProfileButton, AboutButton, TestButton),
      });
    }
  }

  toggleSidebarIsOpenState = () => {
    this.setState({ sidebarIsOpen: !this.state.sidebarIsOpen });
  }

  render() {
    const { authenticated, sidebarIsOpen, sidebarElements} = this.state;
    return (
      <div>
        <Sidebar
          authenticated={authenticated}
          sidebarIsOpen={sidebarIsOpen}
          sidebarElements={_.isEmpty(sidebarElements) ? undefined: sidebarElements}
          toggleSidebarIsOpenState={this.toggleSidebarIsOpenState}
        />
      </div>
    );
  }
}

SidebarContainer.propTypes = {
  authenticated: PropTypes.bool,
};

export default SidebarContainer;

Sidebar.js:

import React from 'react';
import _ from 'lodash';
import PropTypes from 'prop-types'

import SidebarStyles from '../styles/SidebarStyles';

export const Sidebar = (props) => {
  if (props && props.sidebarElements) {
    return (
      <SidebarStyles sidebarIsOpen={props.sidebarIsOpen}>
        {_.map(props.sidebarElements, (value, index) => {
          return React.createElement(
            value,
            {
              key: index,
              authenticated: props.authenticated,
              sidebarIsOpen: props.sidebarIsOpen,
              toggleSidebarIsOpenState: props.toggleSidebarIsOpenState,
            },
          );
        })}
      </SidebarStyles>
    );
  }
  return (
    <div></div>
  );
}

Sidebar.propTypes = {
  authenticated: PropTypes.bool,
  sidebarIsOpen: PropTypes.bool,
  sidebarElements: PropTypes.array,
  toggleSidebarIsOpenState: PropTypes.func,
};

export default Sidebar;

TestButton.js:

import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'react-fontawesome';
import {
  Link
} from 'react-router-dom';

export const TestButton = (props) => {
  return (
    <Link to="/test">
      <Icon name='rocket' size='2x' />
    </Link>
  );
}

export default TestButton;

AboutButton.js:

import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'react-fontawesome';
import {
  Link
} from 'react-router-dom';

export const AboutButton = (props) => {
  return (
    <Link to="/">
      <Icon name='home' size='2x' />
    </Link>
  );
}

export default AboutButton;

No refresh, just constant clicking on the '/test' route from the '/' route:

enter image description here

after refresh:

enter image description here

Edit:

Root components:

Edit:

store.js:

import {
  createStore,
  applyMiddleware,
  compose,
} from 'redux';
import createSagaMiddleware from 'redux-saga';

import { rootReducer } from './rootReducers';
import { rootSaga } from './rootSagas';

// sagas
const sagaMiddleware = createSagaMiddleware();

// dev-tools
const composeEnhancers = typeof window === 'object' && (
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? (
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
  ) : compose
);

export function configureStore() {
  const middlewares = [
    sagaMiddleware,
  ];
  const store = createStore(
    rootReducer,
    {},
    composeEnhancers(applyMiddleware(...middlewares))
  );

  sagaMiddleware.run(rootSaga);
  return store;
}

export const store = configureStore();

index.js (root):

import React from 'react';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';

import { store } from './store';
import AppContainer from 'containers/AppContainer';

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <AppContainer />
    </BrowserRouter>
  </Provider>,
  document.getElementById('root')
);

AppContainer:

import React from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';

import { logout, verifyToken } from './actions';
import { selectAuthenticated, selectAuthenticating } from './selectors';
import AppRoutes from 'routes/AppRoutes';

export class AppContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { loaded: false };
  }

  componentDidMount() {
    const token = localStorage.getItem('jwt');
    if (token) {
      this.props.verifyToken(token, () => this.setState({ loaded: true }));
    } else {
      this.setState({ loaded: true });
    }
  }

  render() {
    if (this.state.loaded) {
      return (
        <AppRoutes
          authenticated={this.props.authenticated}
          authenticating={this.props.authenticating}
          logout={this.props.logout}
        />
      );
    } else {
      return <div>Loading ...</div>
    }
  }
}

function mapStateToProps(state) {
  return {
    authenticated: selectAuthenticated(state),
    authenticating: selectAuthenticating(state),
  };
}

function mapDispatchToProps(dispatch) {
  return {
    verifyToken: (token = '', callback = false) => dispatch(verifyToken(token, callback)),
    logout: () => dispatch(logout()),
  };
}

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppContainer));

Edit 2 for LazyLoad:

services/LazyLoad/index.js:

import React from 'react';

export class LazyLoad extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      AsyncModule: null,
    };
  }

  componentDidMount() {
    this.props.getComponent()  // getComponent={() => import('./someFile.js')}
      .then(module => module.default)
      .then(AsyncModule => this.setState({AsyncModule}))
  }

  render() {
    const { loader, ...childProps } = this.props;
    const { AsyncModule } = this.state;

    if (AsyncModule) {
      return <AsyncModule {...childProps} />;
    }

    if (loader) {
      const Loader = loader;
      return <Loader />;
    }

    return null;
  }
}

export default LazyLoad;
laser
  • 1,388
  • 13
  • 14
Kyle Truong
  • 2,545
  • 8
  • 34
  • 50
  • 1
    I was having a similar problem yesterday on my project. My had a wrapping child component that was using redux and this was blocking the re-render. Fixed it by changing `connect(...)(TopLayout)` to `withRouter(connect(...)(TopLayout))` so it will re-render each time the route changes. More info here: https://reacttraining.com/react-router/web/api/withRouter. You're not showing us your implementation, so I'm not sure it's the same problem. – Tom Van Rompaey Jun 04 '17 at 01:05
  • 1
    This feels so much like the right answer and I want it to be true but it's still not working for me. I went over and wrapped every container and component that had a connect() call with withRouter and it's still giving me the bug. – Kyle Truong Jun 04 '17 at 02:07
  • 1
    This might be because of `LazyLoad`. Can you share the `LazyLoad` code? – Tharaka Wijebandara Jun 04 '17 at 02:08
  • edited to include it – Kyle Truong Jun 04 '17 at 02:09
  • @TomVanRompaey, Thanks! Your comment solved my issue. – Green Aug 17 '17 at 10:23

1 Answers1

3

Your problem lies with LazyLoad component. For both "/" or "test" paths, what AppRoutes component ultimately renders is a LazyLoad component. Because Route and Switch just conditionally render their children. However, React can't differentiate "/" LazyLoad component and "/test" LazyLoad component. So the first time it renders LazyLoad component and invokes the componentDidMount. But when route changes, React consider it as a prop change of previously rendered LazyLoad component. So it just invokes componentWillReceiveProps of previous LazyLoad component with new props instead of unmounting previous one and mount a new one. That's why it continuously show About component until refresh the page.

To solve this problem, if the getComponent prop has changed, we have to load the new module with new getComponent inside the componentWillReceiveProps. So we can modify the LazyLoad as follows which have a common method to load module and invoke it from both componentDidMount and componentWillReceiveProps with correct props.

import React from 'react';

export class LazyLoad extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      AsyncModule: null,
    };
  }

  componentDidMount() {
    this.load(this.props);
  }

  load(props){
    this.setState({AsyncModule: null}
    props.getComponent()  // getComponent={() => import('./someFile.js')}
      .then(module => module.default)
      .then(AsyncModule => this.setState({AsyncModule}))
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.getComponent !== this.props.getComponent) {
      this.load(nextProps)
    }
  }

  render() {
    const { loader, ...childProps } = this.props;
    const { AsyncModule } = this.state;

    if (AsyncModule) {
      return <AsyncModule {...childProps} />;
    }

    if (loader) {
      const Loader = loader;
      return <Loader />;
    }

    return null;
  }
}

export default LazyLoad;
Tharaka Wijebandara
  • 7,955
  • 1
  • 28
  • 49
  • 2
    @KyleTruong I'm glad that it helped you. I'll explain why when I get some time. – Tharaka Wijebandara Jun 04 '17 at 02:57
  • Thank you again, looking forward to it. – Kyle Truong Jun 04 '17 at 03:07
  • This answer really helped me today @TharakaWijebandara. Similar issue except I was seeing a route component rendered when leaving that route (rendering on leave) which caused some issues. Your answer helped me see that the previous route component was being rendered because the same was being re-used. Thank you. – Jamie Dixon Jun 26 '17 at 20:27