How to handle props and states to avoid infinite loops? - javascript

I have a component which gets props from his parents, I am checking if this.props.addMarker is true in componentDidUpdate(), if true it triggers a function in which there is a setState.
You can imagine what's next: the setState triggers the componentDidUpdate() function, which checks if this.props.addMarker is true...and so on...
What should I do to avoid this type of issue?
Here is my code:
componentDidUpdate() {
if(this.props.addMarker) {
const place = this.props.coordinatesToCenter;
const coord = place.coordinates;
this.addMarkerProcess(place.place_name, place.place_type, coord.lon, coord.lat);
}
}
addMarkerProcess(name, maki, xCoordinate, yCoordinate) {
const data = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [xCoordinate, yCoordinate]
},
properties: {
// place:
// login:
lat: yCoordinate,
lon: xCoordinate,
color: "#00FFFF"
}
};
if(this.state.clickIsEmpty) {
data.properties.place = this.state.userNewPlaceInput;
data.properties.login = this.state.userNewTypeInput;
} else {
data.properties.place = name;
data.properties.login = maki;
}
const prevGeoJson = _.cloneDeep(this.state.geoJson);
console.log("prevGeojson1", prevGeoJson)
// map only rerenders geoJSONLayer if geoJSONLayer.data is a new instance:
const geoJson = Object.assign({}, this.state.geoJson);
geoJson.features.push(data);
this.setState(prevState => ({
prevGeoJson: prevGeoJson,
geoJson: geoJson,
currentMarker: data
}));
let canvas = document.querySelector('.mapboxgl-canvas');
if(canvas.classList.contains("cursor-pointer")) {
canvas.classList.remove("cursor-pointer");
}
}

componentDidUpdate() is invoked immediately after an update, so if you call setState() without being wrapped in a condition, you will inevitably cause the infinite loop to occur.
You could call setState() based on a comparison between new and previous props.
componentDidUpdate(prevProps) {
if(this.props.data !== prevProps.data)
this.fetchNewData(this.props.data);
}

Related

setState does not update array state in React Js

I try to get value from the input and put it into object then push to new array. Then combine this array(with setState) with my state array. However, my state array value always returns empty. Could you please say where is the mistake. Thanks.
class AddExtraSeassion extends Component {
constructor(props) {
super(props);
this.checkValidity = this.checkValidity.bind(this);
this.onChangeDate = this.onChangeDate.bind(this);
this.onChangeTime = this.onChangeTime.bind(this);
this.onChangeDuration = this.onChangeDuration.bind(this);
this.state = {
checkedStudentLength: 0,
validity: false,
seassionDuration: null,
seassionDate: null,
seassionTime: null,
showSeassion: false,
seassions: []
}
}
addSeassion = () => {
this.setState({showSeassion: true});
}
onChangeDate = (event) => {
this.setState({seassionDate: event.target.value});
};
onChangeTime = (event) => {
this.setState({seassionTime: event.target.value});
};
onChangeDuration = (event) => {
this.setState({seassionDuration: event.target.value});
};
checkValidity() {
const seassionDate = this.state.seassionDate;
const seassionTime = this.state.seassionTime;
const seassionDuration = this.state.seassionDuration;
const obj = {
"Date": seassionDate,
"Time": seassionTime,
"Duration": seassionDuration
};
let SeassionList=[];
SeassionList.push(obj);
console.log(JSON.stringify(SeassionList) + " SeassionList array"); // print result
this.setState({
seassions: [...this.state.seassions, SeassionList]
})
console.log(JSON.stringify(this.state.seassions) + " state array"); // always empty
As #norbitrial mentioned setState is async operation so console.log() will not show the updated state . If you want to check if the state is updating or not you can use the callback provided by the setState method like below:
this.setState(prevState => ({
seassions: [...prevState.seassions, SeassionList[0]]
}),() => {
console.log(this.state.seassions);
})
The first thing is you should not expect to see the changes in that line of console.log() immediately after your usage of setState() because it is asynchronous action.
Also you can try using the previous state of you array and passing obj instead of SeassionList:
this.setState(prevState => ({
...prevState,
seassions: [...prevState.seassions, obj]
}))

Which approach in React is better?

Below both code does exactly same but in different way. There is an onChange event listener on an input component. In first approach I am shallow cloning the items from state then doing changes over it and once changes are done I am updating the items with clonedItems with changed property.
In second approach I didn't cloned and simply did changes on state items and then updated the state accordingly. Since directly (without setState) changing property of state doesn't call updating lifecycles in react, I feel second way is better as I am saving some overhead on cloning.
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
const clonedItems = Array.from(items);
clonedItems.map((ele: NetworkItem) => {
if (ele.nicType === type) {
ele.rate = Number(value);
}
});
this.setState({ items: clonedItems });
};
OR
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
items.map((ele: NetworkItem) => {
if (ele.nicType === type) {
ele.rate = Number(value);
}
});
this.setState({ items });
};
You can use this
this.setState(state => {
const list = state.list.map(item => item + 1);
return {
list,
};
});
if you need more info about using arrays on states, please read this: How to manage React State with Arrays
Modifying the input is generally a bad practice, however cloning in the first example is a bit of an overkill. You don't really need to clone the array to achieve immutability, how about something like that:
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
const processedItems = items.map((ele: NetworkItem) => {
if (ele.nicType === type) {
return {
...ele,
rate: Number(value)
};
} else {
return ele;
}
});
this.setState({ items: processedItems });
};
It can be refactored of course, I left it like this to better illustrate the idea. Which is, instead of cloning the items before mapping, or modifying its content, you can return a new object from the map's callback and assign the result to a new variable.

Client-side filter failing in subscribe callback

I am currently struggling to solve a seemingly simple issue in my Ionic application. I am loading in a BehaviorSubject of GeoFire objects into the client based on their relation in distance to the user (which is working fine), and then perform a subscription callback on the BehaviorSubject where I assign an array of 'markers' to the objects, and then perform a client side filter based on categories they contain and list them in the DOM. Because the data may be filtered more than once by different categories, there is a markers array and a filteredMarkers array, in which filteredMarkers gets assigned to markers at the beginning of every call of the filtering functon.
The problem is that when the objects are initially loaded in, my current code is seemingly having this filtering function be called before the data has actually loaded in, and then filteredMarkers gets set to to markers before any data has loaded in, thus resulting in an empty list. How do I ensure the data has loaded in? Here is my code:
Page Component:
ionViewDidLoad() {
console.log('ionViewDidLoad LocationsPage');
this.getUserLocation();
this.subscription = this.locationService.hits
.subscribe((hits) =>{
this.markers = hits;
this.filterLocations(this.locationCategory, this.markers)
},
(err)=> {
console.log(err);
});
}
private getUserLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(position => {
this.userLat = position.coords.latitude;
this.userLong = position.coords.longitude;
this.locationService.getLocations(this.radius, [this.userLat, this.userLong]);
console.log(this.locationService.hits);
});
}
}
filterLocations(category: string, markers) {
console.log(markers);
this.filteredMarkers = markers;
console.log('markers to be filtered: ', this.filteredMarkers, 'category: ',category);
if (category != ''){
category = category.toLocaleLowerCase();
this.filteredMarkers = this.filteredMarkers.filter(location => {
console.log(this.filteredMarkers);
return !category || location.category.toLowerCase().indexOf(category) !== -1;
});
}
return this.filteredMarkers;
}
LocationService:
getLocations(radius: number, coords: Array<number>) {
this.geoFire.query({
center: coords,
radius: radius
})
.on('key_entered', (key, location, distance) => {
console.log(newName);
let hit = {
location: location,
distance: distance,
name: '',
description: '',
category: ''
};
let currentHits = this.hits.value;
this.getLocationData(key).subscribe(
(location) =>{
newName = location.name;
hit.name = location.name;
hit.description = location.description;
hit.category = location.category;
currentHits.push(hit);
this.hits.next(currentHits);
}
);
});
}
getLocationData(ID: string): Observable<Location>{
return this.db.object('/locationData/'+ID).valueChanges();
}

Using spread operator to setState in multiple succession

I have some values stored in local storage. When my component mounts, I want to load these values into the state. However, only the last property being added is added to the state. I've checked the values on my localStorage, and they are all there. Furthermore, when I log the variables (desc, pic or foo) in the condition block, they are there.
I thought at first each subsequent if block is re-writing the state, but this is not the case as I am using the spread operator correctly (I think!), adding the new property after all pre-existing properties.
I think the problem is that the code in the last if block is running before the state is set in the first if block. How do I write the code so I get all three properties from my local storage into the state?
//what I expect state to be
{
textArea: {
desc: 'some desc',
pic: 'some pic',
foo: 'some foo'
}
}
//what the state is
{
textArea: {
foo: 'some foo'
}
}
componentDidMount () {
const desc = window.localStorage.getItem('desc');
const pic = window.localStorage.getItem('pic');
const foo = window.localStorage.getItem('foo');
if (desc) {
console.log(desc) //'some desc'
this.setState({
...this.state,
textArea: {
...this.state.textArea,
desc: desc,
},
}, ()=>console.log(this.state.textArea.desc)); //undefined
}
if (pic) {
console.log(pic) //'some pic'
this.setState({
...this.state,
textArea: {
...this.state.textArea,
pic: pic,
},
}, ()=>console.log(this.state.textArea.pic)); //undefined
}
if (foo) {
console.log(foo) //'some foo'
this.setState({
...this.state,
textArea: {
...this.state.textArea,
foo: foo,
},
}, ()=>console.log(this.state.textArea.foo)); //'some foo'
}
}
You are likely being caught by React batching setState calls by shallow-merging the arguments you pass. This would result in only the last update being applied. You can fix this by only calling setState once, for example:
componentDidMount () {
const desc = window.localStorage.getItem('desc');
const pic = window.localStorage.getItem('pic');
const foo = window.localStorage.getItem('foo');
this.setState({
textArea: Object.assign({},
desc ? { desc } : {},
pic ? { pic } : {},
foo ? { foo } : {}
)
});
}
The other version is to pass an update function to setState rather than an update object, which is safe to use over multiple calls. The function is passed two arguments: the previous state, and the current props - whatever you return from the function will be set as the new state.
componentDidMount () {
const desc = window.localStorage.getItem('desc');
const pic = window.localStorage.getItem('pic');
const foo = window.localStorage.getItem('foo');
this.setState(prevState => {
if (desc) {
return {
textArea: {
...prevState.textArea,
desc
}
}
} else {
return prevState;
}
});
// Repeat for other properties
}
It's a little more verbose using this approach, but does offer the opportunity to extract state updating functions outside of your component for testability:
// Outside component
const updateSubProperty = (propertyName, spec) => prevState => {
return {
[propertyName]: {
...prevState[propertyName],
...spec
}
}
}
const filterNullProperties = obj => {
return Object.keys(obj).reduce((out, curr) => {
return obj[curr] ? { ...out, [curr]: obj[curr] } : out;
}, {});
}
componentDidMount () {
this.setState(updateSubProperty("textArea",
filterNullProperties(
desc: window.localStorage.getItem('desc'),
pic: window.localStorage.getItem('pic'),
foo: window.localStorage.getItem('foo')
)
));
}
This way adds some complexity, but (in my opinion) gives a really readable component where it is clear to our future selves what we were trying to achieve.

React not passing state to child via props

I am trying to pass my parent App state to a child component Chart.
App
constructor() {
super();
this.state = {
dataPoints: {
'424353': {
date: '10/10/2016',
qty: '95'
},
'535332': {
date: '10/11/2016',
qty: '98'
},
'3453432': {
date: '10/01/2017',
qty: '94'
}
}
};
this.addPoint = this.addPoint.bind(this);
}
addPoint(dataPoint) {
let dataPoints = {...this.state.dataPoints};
const timestamp = Date.now();
dataPoints[timestamp] = dataPoint;
this.setState({ dataPoints: dataPoints });
console.log('new state', this.state.dataPoints);
}
render() {
return (
<div className="app">
<Chart dataPoints={this.state.dataPoints} />
<FormControl addPoint={this.addPoint} />
</div>
);
}
Chart
composeData() {
Object
.keys(this.props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
componentWillUpdate() {
this.composeData();
}
The addPoint method works, i.e. I can see in the React console that the new datapoint is added to the state. But it is not reflected in the Chart component. More oddly (to me) is the fact that when I've added a point, the console.log line in my addPoint method (above):
console.log('new state', this.state.dataPoints)
does not show the new data point.
In add point
addPoint(dataPoint) {
let dataPoints = {...this.state.dataPoints};
const timestamp = Date.now();
dataPoints[timestamp] = dataPoint;
this.setState({ dataPoints: dataPoints });
console.log('new state', this.state.dataPoints);
}
In the above code you do not see the updated value because setState takes time to mutate, You must log it in the setState call back
this.setState({ dataPoints: dataPoints }, function(){
console.log('new state', this.state.dataPoints);
});
And also in the Chart component you need to bind the composeData function if you are using this.props inside it like
composeData = () =>{
Object
.keys(this.props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
However componentWillMount is only called once and hence you will want to call the composeData function from componentWillReceiveProps also like
componentWillReceiveProps(nextProps) {
this.composeData(nextProps)
}
componentWillMount() {
this.composeData(this.props.dataPoints)
}
composeData(props){
Object
.keys(props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
Because setState is asynchronous, use this you see the updated values:
this.setState({ dataPoints: dataPoints }, () => {
console.log('new state', this.state.dataPoints);
});
Second thing is, whenever any change happen to props values, componentWillReceiveProps lifecycle method will get called, do the computation in this method once you add the new item. Like this:
componentWillReceiveProps(newProps) {
console.log('update props values', newProps);
Object
.keys(newProps.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
Check this answer for complete explanation: why setState is asynchronous.

Categories

Resources