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.
Related
i am having this handleCheckClick funtion witch gets Data i want to store the data into a state every time the handleCheckClick funtion is called so after many times handleCheckClick is called the state should look like the object array below
handleCheckClick = (e, stateVal, index) => {
let prevState = [...this.state[stateVal]];
prevState[index].positive = e.target.checked;
console.log(index);
this.setState({ [stateVal]: prevState });
var date = moment(this.state.dateState).format("YYYY-MM-DD");
const { id, checked } = e.target.dataset;
console.log(stateVal);
if (e.target.checked) {
var checkbox = "True";
} else {
var checkbox = "False";
}
const Data = {
boolvalue: checkbox,
date: date,
userid: id,
};
this.setState({ datastate : Data });// something like this
};
after many times the handleCheckClick funtion is called the state must look like this
[
{
"date" : "2022-02-15",
"userid" : 6,
"boolvalue" : true
},
{
"date" : "2022-02-15",
"userid" : 5,
"boolvalue" : false
},
{
"date" : "2022-02-15",
"userid" :7,
"boolvalue" : true
},
{
"date" : "2022-02-15",
"userid" : 11,
"boolvalue" : true
},
{
"date" : "2022-02-15",
"id" : 4,
"boolvalue" : false
}
]
pls create a codesandbox example
https://codesandbox.io/s/recursing-wind-mjfjh4?file=/src/App.js
You have to take your data and call setState using the existing data merged with the new Data object. The merging can be done using ... (spread) operator. Here's the code with the relevant parts:
class Component extends React.Component {
handleClick = (e) => {
// build up a new data object:
if (e.target.checked) {
var checkbox = "True";
} else {
var checkbox = "False";
}
const { id } = e.target.dataset
var date = moment(this.state.dateState).format("YYYY-MM-DD");
const Data = {
boolvalue: checkbox,
date: date,
userid: id,
};
// set the new state, merging the Data with previous state (accesible via this.state)
// this creates a new array with all the objects from this.state.datastate and the new Dataobject
this.setState({
datastate: [...this.state.datastate, Data]
})
}
// log the state on each update for seeing changes.
componentDidUpdate() {
console.log('Component did update. State:', this.state)
}
// Rendering only a button for showcasing the logic.
render() {
return <button onClick={this.handleClick}></button>
}
constructor(props) {
super(props)
// initialise an empty state
this.state = {
datastate: [],
dateState: new Date()
}
}
}
Edit for removing an element when unchecked:
You can remove a certain element by its id in the onClick handler when the box is unchecked:
class Component extends React.Component {
handleClick = (e) => {
// get id first.
const { id } = e.target.dataset
// if element is not checked anymore remove its corresponding data:
if(e.target.checked === false) {
// remove the element by filtering. Accept everything with a different id!
const update = this.state.datastate.filter(d => d.userid !== id)
this.setState({
datastate: update
})
// end handler here..
return
}
// if we get here, it means checkbox is checked now, so add data!
var date = moment(this.state.dateState).format("YYYY-MM-DD");
const Data = {
// it is better to keep the boolean value for later use..
boolvalue: e.target.checked,
date: date,
userid: id,
};
// set the new state, merging the Data with previous state (accesible via this.state)
// this creates a new array with all the objects from this.state.datastate and the new Dataobject
this.setState({
datastate: [...this.state.datastate, Data]
})
}
// log the state on each update for seeing changes.
componentDidUpdate() {
console.log('Component did update. State:', this.state)
}
// Rendering only a button for showcasing the logic.
render() {
return <button onClick={this.handleClick}></button>
}
constructor(props) {
super(props)
// initialise an empty state
this.state = {
datastate: [],
dateState: new Date()
}
}
}
Feel free to leave a comment
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.
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);
}
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.
I made a component that load data via xhr on the user select a value of <select> element.
class SomeComponent extends Component {
state = {
data: [],
currentCategory: 'all'
}
switchCategory = (ev) => {
console.log('Selected category is ' + ev.target.value);
this.setState({
currentCategory: ev.target.value
});
this.loadData();
}
loadData = async () => {
let { currentCategory } = this.state;
// Always print previous value!!!
console.log(currentCategory);
// Get data via XHR...
}
render() {
return (
<div>
<select value={currentCategory} onChange={this.switchCategory}>
<option value="all">All</option>
{categories.map( category =>
<option key={category._id} value={category.category}>{category.display}</option>
)}
</select>
<table>
// ... prints data with this.state.data
</table>
</div>
);
}
}
Above code is just in brief. Code is quite simple, I just synchronize a value of the select element with this.state.currentCategory, and detect it's switching with switchCategory method of the class.
But the main problem is that when I access the component's state, it always contains previous value, not a present. You can see that I updating the currentCategory on value of the select changes.
switchCategory = (ev) => {
console.log('Selected category is ' + ev.target.value);
this.setState({
currentCategory: ev.target.value
});
this.loadData();
}
So in this situation, this.state.currentCategory must not has "all", like something else "Apple", but still it contains "all", not an "Apple"!
loadData = async () => {
let { currentCategory } = this.state;
// Always print previous value!!! I expected "Apple", but it has "all"
console.log(currentCategory);
// Get data via XHR...
}
So eventually XHR occurs with previous value, and it gives me wrong data that I didn't expected.
After that, choosing other value of the select(let's call it Banana), it has an Apple, not a Banana!
As I know setState is "sync" job, so calling this.switchCategory will happens after updating states, so it must have present value, not a previous.
But when I print the component's state in console, it isn't.
So, what am I missing? Why am I always getting old data, not present? If I doing something wrong approach, then what alternatives can I do?
Any advice will very appreciate it. Thanks!
The problem here is that setState is async (it can be sync in certain situations). That's why you are get previous value.
There are two possible solutions.
//
// 1. use value directly.
//
switchCategory = (ev) => {
this.setState({ currentCategory: ev.target.value });
this.loadData(ev.target.value);
}
loadData = async (currentCategory) => {
console.log(currentCategory);
// Get data via XHR...
}
//
// 2. use completition callback on `setState`.
//
switchCategory = (ev) => {
this.setState({ currentCategory: ev.target.value }, () => {
this.loadData(ev.target.value);
});
}
loadData = async () => {
const { currentCategory } = this.state;
console.log(currentCategory);
// Get data via XHR...
}
Article on synchronous setState in React [link]