Reputation: 987
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
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
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
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
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