How to deal with React state when componentDidUpdate is not triggered even if redux state is changing and connecting with the form as props

| In Articles | 17th July 2019

In the last months we had to deal with an unusual problem. It seems we are not the only developers getting into this. Some other people asked on the interned for a solution for the next problem, but we couldn't see a clear answer.

Sometimes a component props might update but componentDidUpdate not fire. We will show the next example we have.

In our project, a completely separate frontend project based on APIs, we don't store any data on localStorage for safety reasons. We save a minimalist amount of data as cookies for the same reason.

Any other data is retrieved from the backend when the componentDidMount is called and when an object is updated on the database. If is some data that has to be used at the app level, Redux, with the help of actions and reducers will store this data in the app state. We basically send an updated object and we get back the json representing the same thing from the data base.

Not storing data on the computer of the user, means we need to take care of data to be loaded on each component before is used. If the page is not hard refreshed, the state will persist. We only request fresh data from the API when is a refresh or when something is updated.

As we have Parents component and Child Components, and because data does not persist between React component, on refresh, on different parts of the app, we get different type of data.

Our app has a parent component named Main. As this app is about customers and because it has users that will administrate the customers, we check things related to user and customer inside this Main component. If the customer can't do certain things, Main will show to the user a child Component to view something. If the customer can do something else, will show a different Component.

In this way, Main will have as Children Component, multiple other Component. Eq.: Details (to see the details of a customer), or Checks (where a customer is asked to take some needed actions if he is blocked or he didn't paid)

When Main component will render, will render first time with empty data.

componentDidMount of Main will be called next and inside of it we do an API call to get the data from the data base about the Customer.

//import libs
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import { connect } from 'react-redux'
import axios from './utils/Http';
import { customerFetchSuccess, customerFetchFailure } from './actions'
class Main extends Component {
  
  constructor(props) {

    super(props);

    this.state = {
    };

    this._isMounted = false;
    this.reCheckSomething = this.reCheckSomething.bind(this);
  }

  reCheckSomething() {
	//do things inside
  }

  componentDidMount() {

    this._isMounted = true;

	await axios.get('customer')
	.then(response => {	
	  this.props.dispatch(customerFetchSuccess(response)); 
	}).catch(error => {
	  if(error){
		this.props.dispatch(customerFetchFailure(error))
	  }
	})
  }

  componentDidUpdate(prevProps){
    if(this._isMounted){
        //if customer is loaded
        if(this.props.customer != prevProps.customer){
			//call a function to do things
			this.reCheckSomething();
        }
}
  }

  componentWillUnmount() {
    this._isMounted = false;	
  }
  
  render() {
		
	//get the customer from the props
    const { customer } = this.props;   
    
	if(customer && customer.hasAttribute){
	  //give access to some child components, do redirections to some pages
	}else{
	  //stop access to some child components, do redirections to other pages
	}
	
    return (
      <div>     
        <div className="header" id="header" ref={this.headerRef}></div>        
        <main id="main" role="main">
          { this.props.children }
        </main>
        <div className="footer" ref={this.footerRef}></div>
      </div>
    )
  }
}

Main.displayName = displayName

const mapStateToProps = state => {
	return {
		customer: state.customer,
	}
}

Main.propTypes = {
  customer: PropTypes.object.isRequired
}

export default connect(mapStateToProps)(Main)

In this poin, customer object will be mapped into props and accessible as props

In the situation when, some other functions will need to fire after the data is received, this function, let's name it Checks, have to be called inside componetDidUpdate inside a check that will say that this.props.customer != prevProps.customer

// import libs
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { compose } from 'redux'
import { connect } from 'react-redux';

const displayName = 'Checks';

// initialize component
class Checks extends Component {

  // validate props
  static propTypes = {
    customer: PropTypes.object.isRequired
  }

  constructor(props) {
    super(props)

    this.state = {
      loading: false,
      error: '',
      success: ''
    }

    this._isMounted = false;
    this.doSomething = this.doSomething.bind(this);
  }
  
  componentDidMount() {
    this._isMounted = true;
	//customer should exist into the state from Main
	//With all of that, this Child component will initialize with empty data and custome will be loaded on the second render.
	//In this specific case, for us, componentDidUpdate was not called, because the customer was already loaded, so this.props.customer ws the same as prevProps.customer
	//not in mapStateToProps, where customer was intially empty and after had data
  }

  componentDidUpdate(prevProps) {
    
    //in our case, this will only fire when customerComplete is changing
    if (this.props.customer.id != prevProps.customer.id) {    
      this.doSomething();      
    }

  }

  componentWillUnmount() {
    this._isMounted = false;
  }
  
  doSomething() {
	//do something with the customer after is loaded
  }

  // render component
  render() {
    const { customer } = this.props;
	//customer can can be populated from the start here, but mapStateToProps can show an empty on the first render and a complete customer on the second render -> weird To be able to trigger a function on componentDidUpdate, we need to check for customer.id and define customerComplete bellow. When customerComplete will change, componentDidUpdate will be called

    return ( 
      //show things here
      )
  }
}

const mapStateToProps = (state, ownProps) => {
  const {customer} = state;
  let customerComplete = false;
  if(customer.id){//id will be null initially and populated on the second render
    customerComplete = true;
  }
  return {    
    customerComplete: customerComplete,
    customer: customer,
  }
}

export default compose(
    connect(mapStateToProps, null),
    reduxForm({
      form: 'checks', // a unique identifier for this form
      enableReinitialize: true,
      destroyOnUnmount: false
    })
)(Checks);

A child component of Main, might also need the same data. In this situation, we don't send the customer as props to the Child Component, because not all Child Components of Main will need that data, but instead, we do a new request to backend and receive the customer, if is not into the state.

We had moments in our application when the customer was loaded already into the Child component and componentDidUpdate didn't run because it didn't know that something was changed into the props.

The Redux connect instead, had initially an empty customer object and after the data was loaded, customer was mapped into the Child props.

We realised that the only way to solve this problem was to notify the Component that the props have been changed, even if componentDidUpdate did not fire.

Our solution was to check when the state inside mapStateToProps will change and define a new variable customerComplete which initially is false but will become true when customer will be loaded.