componentDidMount() is not getting called but only

2020-07-10 12:12发布

问题:

Just a heads up: this is a really strange problem. I'll do my best to clearly explain the issue. This is happening in my ReactJs app using React Router.

I have some components that require some type of parameter coming from the URL. I also have components that do NOT depend on a parameter.

If I go to components that do not require any parameters, there are no problems whatsoever.

So, I go to my Home, then Account List, neither of which require any parameters, I can go back and forth as many times as I like, there's no problem.

However, if I go to a component that uses a parameter, then try to go another component that uses a parameter, I then get an error. The error indicates that the component react-router is supposed to load is NOT mounted properly which throws an error telling me that the data the child component needs is missing. The errors I'm getting are pretty simple ones. They simply say something like:

this.props.something is required but undefined

Here's my routes in App.jsx:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Route, Switch, withRouter } from 'react-router-dom';

// Import components here
// Just showing one for brevity
import Home from '../components/home/Home';

import * as appActions from '../actions/app-actions';

class App extends Component {

   constructor(props) {

      super(props);
   }

   render() {

      return(

          <div>
              <Switch>
                 <Route exact path="/member" component={Home} />
                 <Route exact path="/member/accounts" component={Accounts} />
                 <Route exact path="/member/projects" component={ProjectsList} />
                 <Route path="/member/projects/profile/:id" component={ProjectProfile} />|
                 <Route exact path="/member/tasks" component={TasksList} />
                 <Route path="/member/tasks/profile/:id" component={TaskProfile} />
              </Switch>
          </div>
      );

   }
}

function mapStateToProps(state) {

    return {
        member: state.member.memberData
    }
}

function mapDispatchToProps(dispatch) {

    return {

        actions: bindActionCreators(appActions, dispatch)
    };
}

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

As you can see from the routes, both ProjectProfile and TaskProfile components require ID's. Furthermore, both ProjectProfile and TaskProfile components are rather simple parent components that do two things:

  1. Make an API call to load data in the componentDidMount() event
  2. They also load the correct child component based on user's screen resolution

Here's my ProjectProfile component and the TaskProfile is pretty much identical to this.

import React, { Component } from 'react'
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

// Actions
import * as projectProfileActions from '../../../actions/project-profile-actions';

// Components
import Desktop from './ProjectProfileDesktop';
import Mobile from './ProjectProfileMobile';

class ProjectProfile extends Component {

    constructor(props) {

        super(props);
    };

    componentDidMount() {

        const id = this.props.match.params.id;
        this.props.actions.getData(id);
    }

    render() { 

        return (
                <div className="height-100 width-100">
                    <div className="height-100 width-100 row row-clean">
                    {this.props.ui.isDesktop || this.props.ui.isTablet ? <Desktop />
                        : this.props.ui.isMobile ? <Mobile />
                        : null}
                    </div>
            </div>
        );
    }
}

function mapStateToProps(state) {
    return {
        ui: state.app.window
    };
}

function mapDispatchToProps(dispatch) {

    return {
        actions: bindActionCreators(projectProfileActions, dispatch)
    };
}

export default connect(mapStateToProps, mapDispatchToProps)(ProjectProfile);

The strangest part about this is that if I keep going back and forth between ProjectProfile and any other component and stay away from TaskProfile, there are no problems. I can even hit ProjectProfile with different ID's and everything works fine. It all fails only when I go to another component with parameter. I can do the same with TaskProfile. I can keep hitting TaskProfile with different ID's OR go back and forth between TaskProfile and any component without a parameter and everything works fine.

Only if I go to ProjectProfile first, then try to go to TaskProfile or vice versa, the error occurs.

The error is simply telling me that the data ProjectProfileDesktop requires is not there.

Further inspection shows me that after loading a component with a parameter, if I go to another component with a parameter, I'm just NOT hitting the componentDidMount() method in the second component. Looks like something in the render() method is causing an issue and preventing it from going to componentDidMount(). I'm not sure what the issue is because I'm not getting any errors. I put a debugger at the top of the render() method. Though I'm not seeing an exact error, the flow shows me that something is definitely going wrong.

Please also notice that I'm using withRouter. I've seen some issues involving withRouter but again, I couldn't find anything concrete so far.

I also want to mention that I also have Stripe provider wrapping my App component. Not sure if this is playing a role in this problem as well. Here's what the render() looks like in my index.js:

render(
   <Provider store={store}>
        <BrowserRouter history={browserHistory}>
          <StripeProvider apiKey="my_key">
              <App />
          </StripeProvider>
       </BrowserRouter>
   </Provider>,
   document.getElementById('root')
);

I'd appreciate some pointers on this.

UPDATE:

A behavior I'm observing is that after loading a component with a param, when I hit the second component with a param, I hit the render() method first and right after the first createElement, the code jumps to ReactInstrumentation.debugTool.onBeginLifeCycleTimer -- see screen shot below.

UPDATE 2:

At this point, I'm convinced the issue is not caused by react-router because I hard-coded the ID I needed into the component so that I don't depend on react-router or anything else to get it.

I also removed /:id parameter from the routes for my TaskProfile and ProjectProfile components.

I still get the same behavior. So, something is preventing componentDidMount() method from being called. I also tried componentDidUpdate() to see if it was being called and it is NOT. I placed componentWillUnmount() in both components and as I navigate elsewhere in the app, in both components componentWillUnmount() gets called so one could argue that both components are unmounting OK.

So the bottom line is that something in the render() method is preventing componentDidMount() or any other lifecycle method from being called but this is only happening in this particular circumstance.

I'd appreciate any suggestions at this point!!!

回答1:

It's been more than a week I've been banging my head against the wall on this issue and it's finally solved. Admittedly, lots of concepts that I thought I understood became even clearer in the process.

The error was due a logical error on my part that handled "isLoading" animation logic -- yes, as simple and stupid as that!!!

What I really wanted to share here are the lessons I've learned for everyone who may experience a similar behavior in the future.

  1. You should hit componentDidMount() every time you load your component. Even if you go back and forth to load the same component. But you hit componentDidMount() ONLY IF your component was unmounted in the first place. If the component does not get unmounted, you will NOT hit componentDidMount() in your subsequent uses of the component. So look at your logic carefully to see if your component gets unmounted if you navigate away from it. I think the easiest way to see if this is happening is by adding componentWillUnmount() method with a debugger in it. If you're hitting this method when you navigate away from your component, that means it is unmounting.
  2. Did lots of research during this process to see where the best place to make API calls to fetch data and it's definitely clear that componentDidMount() is still the best place to do it. Having said that you may need additional logic in your componentDidMount() to make sure you don't make any unnecessary API calls because your data may still be in your store.
  3. During my research, some suggested that it's better to use componentWillReceiveProps() method to make my API call to fetch data. This is bad advice! As a matter of fact, componentWillReceiveProps(), componentWillUpdate() and componentWillMount() methods are being deprecated so we should not use them for two reasons: future compatibility and following best practices. Here's more info on why they're being deprecated: https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html
  4. Maybe the most important lesson is this: the cause of the problem may be much simpler than you think -- it usually is -- and the logic in your code may be the cause of the issue -- it usually is!

Hope these lessons will help others find their solutions quickly and painlessly!



回答2:

ComponentDidMount it's called just one time, when the component is mounted. As you are using a prop to then make an api call, I would use componentWillReceiveProps and check props and then make an api call.

If you want yo check that the componentDidMount is used by your flow, use a console.log to check it.

Regards!