barrylachapelle
barrylachapelle

Reputation: 987

Comparing PrevProps in componentDidUpdate

I am trying to detect when a prop has changed inside componentDidUpdate of a mounted component. I have a test (refreshData in the code below) that is working fine. Is it possible to SOMEHOW pass props in a way that aren't detected by componentDidUpdate(prevProps)?

In component.js:

componentDidUpdate(prevProps){

    //works fine
    if ( this.props.refreshData !== prevProps.refreshData ) {
        if ( this.props.refreshData )
            this.refreshData();
    }

    //these two arent calling
    if ( this.props.selectedCountries !== prevProps.selectedCountries ) {
        if ( this.props.selectedCountries )
            console.log('updated selected countries');
    }

    if ( this.props.selectedLocations !== prevProps.selectedLocations ) {
        console.log('updated selected locations');
    }

}

and in App.js passing the props like:

selectLocation = (id, type, lng, lat, polydata, name, clear = false) => {

  //console.log(type);
  //console.log(lng);
  //console.log(lat);
  //console.log(polydata);

  let selectedType = 'selected' + type;
  let previousState = [];

  if (clear) {
    this.setState({
      selectedCountries: [],
      selectedLocations: [],
      selectedServices: [],
      selectedPoints: [],
      mapCenter: [lng, lat],
      locationGeoCoords: [polydata]
    })
    previousState.push(id);

  } else {

    previousState = this.state[selectedType];
    if (previousState.indexOf(id) === -1) {
      //push id
      previousState.push(id);

    } else {
      //remove id
      var index = previousState.indexOf(id)
      previousState.splice(index, 1);
    }
  }

  if (type === "Countries") {

    this.setState({
      selectedCountries: previousState,
      refreshData: true,
    })
  } else if (type === "Locations") {
    this.setState({
      selectedLocations: previousState,
      refreshData: true
    })
  } else if (type === "Points") {
    this.setState({
      selectedPoints: previousState,
      refreshData: true
    })
  }


}


render() {
  return (

    <component
      selectedCountries={this.state.selectedCountries}
      selectedLocations={this.state.selectedLocations}
      refreshData={this.state.refreshData} />
  }
}

Upvotes: 16

Views: 42733

Answers (4)

Jose Quijada
Jose Quijada

Reputation: 692

I ran into this very issue. My solution was to send downstream to child components a clone of the state in question. This way when the state changes in App.js again, it will not affect the copy of the state passed down to children since those children were given a clone. In the previous props passed to async componentDidUpdate (prevProps) in child components, prevProps will be the clone that was originally handed down, and current props will be the most recent state changes made in App.js, which again is a clone, but prev and current props will be different.

Below is snipppet from App.render(), notice the value assign to the filter attribute, namely a clone the portion of the state in question:

...
<Routes onCategorySelect={this.handleCategorySelect}
                onCategoryUnselect={this.handleCategoryUnselect}
                onRouteLoad={this.handleRouteLoad} filter={this.cloneFilter(savedState.filter)}
                updatedDimension={this.state.updatedDimension}/>
...

And this is the componentDidUpdate() of the child component:

async componentDidUpdate (prevProps) {
    if (this.props.filter !== prevProps.filter && this.props.updatedDimension !== this.dimension) {
      await this.updateChart()
    }
  }

Upvotes: 0

Rose Robertson
Rose Robertson

Reputation: 1256

Hi :) as noted in my comment, the issue is in your App.js file - you are mutating an array. In other words, when you THINK you are creating a new array of selected countries to pass down, you are actually updating the original array, and so when you go to do a comparison you are comparing the two exact same arrays ALWAYS.

Try updating your App.js like so -

selectLocation = (id, type, lng, lat, polydata, name, clear = false) => {

  //console.log(type);
  //console.log(lng);
  //console.log(lat);
  //console.log(polydata);

  let selectedType = 'selected' + type;
  let previousState = [];

  if (clear) {
    this.setState({
      selectedCountries: [],
      selectedLocations: [],
      selectedServices: [],
      selectedPoints: [],
      mapCenter: [lng, lat],
      locationGeoCoords: [polydata]
    })
    previousState.push(id);

  } else {

    previousState = [].concat(this.state[selectedType]);
    if (previousState.indexOf(id) === -1) {
      //push id
      previousState.push(id);

    } else {
      //remove id
      var index = previousState.indexOf(id)
      previousState.splice(index, 1);
    }
  }

  if (type === "Countries") {

    this.setState({
      selectedCountries: previousState,
      refreshData: true,
    })
  } else if (type === "Locations") {
    this.setState({
      selectedLocations: previousState,
      refreshData: true
    })
  } else if (type === "Points") {
    this.setState({
      selectedPoints: previousState,
      refreshData: true
    })
  }


}


render() {
  return (

    <component
      selectedCountries={this.state.selectedCountries}
      selectedLocations={this.state.selectedLocations}
      refreshData={this.state.refreshData} />
  }
}

The only difference is the line where you set previousState - I updated it to be

previousState = [].concat(this.state[selectedType]);

By adding the [].concat I am effectively creating a NEW array each time and so then when you apply your changes to the array via push/splice you will be only modifying the NEW array. Then the comparison will work properly once you pass it down as props :)

For your reading interest, I found a post that talks about this a bit: https://medium.com/pro-react/a-brief-talk-about-immutability-and-react-s-helpers-70919ab8ae7c

Upvotes: 19

grenzbotin
grenzbotin

Reputation: 2565

did you make sure that the props of locations & countries are actually changing? If yes, the following code should work:

componentDidUpdate(prevProps) {
  if (this.props.selectedCountries.length !== prevProps.selectedCountries.length) {
    console.log("updated selected countries");
  }

  if (this.props.selectedLocations.length !== prevProps.selectedLocations.length) {
    console.log("updated selected locations");
  }
}

I created a fiddle for showcasing the effect here: https://codesandbox.io/s/o580n8lnv5

Upvotes: 1

Sushanth --
Sushanth --

Reputation: 55740

selectedCountries and selectedLocations are array objects. The reference of it never changes. Instead check for the length.

componentDidUpdate(prevProps){

    if ( this.props.refreshData !== prevProps.refreshData ) {
        if ( this.props.refreshData )
            this.refreshData();
    }

    if ( this.props.selectedCountries.length > prevProps.selectedCountries.length ) {
        if ( this.props.selectedCountries )
            console.log('updated selected countries');
    }

    if ( this.props.selectedLocations.length > prevProps.selectedLocations.length ) {
        console.log('updated selected locations');
    }
}

In the code snippet above, you seem to be making changes to this.state directly. State should be immutable. Always make sure, you concat to add and filter to delete the elements as they create a new array instead of mutating the original array in the state. I would do something in these lines.

Also it is a good practice to capitalize the component name.

selectLocation = (id, type, lng, lat, polydata, name, clear = false) => {
        //console.log(type);
        //console.log(lng);
        //console.log(lat);
        //console.log(polydata);

        let selectedType = "selected" + type;
        let previousState = [];
        let updatedData = [];

        if (clear) {
          this.setState({
            selectedCountries: [],
            selectedLocations: [],
            selectedServices: [],
            selectedPoints: [],
            mapCenter: [lng, lat],
            locationGeoCoords: [polydata]
          });
        } else {
          const data = this.state[selectedType];
          if (data.indexOf(id) === -1) {
            //push id
            updatedData = [...data, id];
          } else {
            updatedData = data.filter((value) => value !== id);
          }
        }

        if(type) {
          this.setState({
            [selectedType]: updatedData,
            refreshData: true
          });
        }
      };

      render() {
        return (
          <component
            selectedCountries={this.state.selectedCountries}
            selectedLocations={this.state.selectedLocations}
            refreshData={this.state.refreshData}
          />
        );
      }
    }

Upvotes: 4

Related Questions