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]
}))
Related
I'm new to React. I'm having the next problem...
At my functional component I have many states, there are 2 that have the sames fields (one is for an auxiliary operation)
const [fieldsToEdit, setFieldsToEdit] = useState({}); // This one get populated after the first render
const [auxFields, setAuxFields] = useState({.....})
Now, I have a button that calls a function, this functions just edits the 'fieldsToEdit', but it is editing the auxFields too! I realized this writing console.logs after and before of the setState call.
const updateEditHandler = (event) => {
event.persist());
setFieldsToEdit((prevState) => {
const { name, value } = event.target;
if(name === "fecha_presentacion")
prevState[name] = value;
else
prevState[name] = Number(value);
return ({
...prevState
});
}
Am I doing it wrong? Hope you can help me.
You should not mutate state. Instead create a new object without modifying the previous one.
prevState[name] = value;
return { ...prevState };
The above first mutates the previous state, then returns a copy of it. Instead return a copy that contains the new value without modifying the previous state.
return { ...prevState, [name]: value };
The above copies the previous state and adds or overrides the (evaluated) name property with value. This is all done without mutating prevState.
Applying this to your actual code you would get the following.
setFieldsToEdit((prevState) => {
const { name, value } = event.target;
if (name == "fecha_presentacion") {
return { ...prevState, [name]: Number(value) };
} else {
return { ...prevState, [name]: value };
}
});
// or (depending on preference)
setFieldsToEdit((prevState) => {
let { name, value } = event.target;
if (name == "fecha_presentacion") value = Number(value);
return { ...prevState, [name]: value };
});
setFieldsToEdit((prevState) => {
const { name, value } = event.target;
if(name === "fecha_presentacion")
prevState[name] = value;
else
prevState[name] = Number(value);
return ({
...prevState
});
In prevState[name] = you mutating the state. You need to clone it and set it with either lodash deepClone or JSON.stringify and then JSON.parse if you are not familiar with lodash.
You are not sharing your code so useState({.....}) does not mean anything.
But from the general picture I am getting from your code I think that fieldsToEdit and auxFields have the same reference so because you prevState[name] = you are changing both
Try to not mutate your state in any framework
I started working with reactjs recently and I know setState() is asynchronous, but if I have the following code below:
// the state is an object
const [addressState, setAddress] = useState({
adrStNumber: null,
adrStreet: null,
adrCity: null,
adrState: null,
adrZipcode: null,
adrCountry: null
})
// this function is called when I select an option in the input
function getAddressAndSaveToState(address) {
address.map(adr => {
if (adr.types.includes('street_number')) {
const adrStNumber = adr.long_name
setAddress({ ...addressState, adrStNumber: adrStNumber })
}
if (adr.types.includes('route')) {
const adrStreet = adr.long_name
setAddress({ ...addressState, adrStreet: adrStreet })
}
if (adr.types.includes('administrative_area_level_1')) {
const adrState = adr.long_name
setAddress({ ...addressState, adrState: adrState })
}
})
}
Great, after the first if, we have our state like this:
{
adrCity: null
adrCountry: null
adrStNumber: "65"
adrState: null
adrStreet: null
adrZipcode: null
}
But because setState is asynchronous, the second if, the ...addressState is still with its initial value (adrStNumber from the last iteration is null), so after the iteration the state will look like this:
{
adrCity: null
adrCountry: null
adrStNumber: null
adrState: "New York"
adrStreet: null
adrZipcode: null
}
In this example, everytime there is a if iteration, I need to add something to my state object. What is the best way to implement something like this?
You can buld your object inside your function's body and assign it to the state after all if are checked. Something like below:
// this function is called when I select an option in the input
function getAddressAndSaveToState(address) {
address.map(adr => {
let newAddress = {...addressState}
if (adr.types.includes('street_number')) {
const adrStNumber = adr.long_name
newAddress.adrStNumber = adrStNumber
}
if (adr.types.includes('route')) {
const adrStreet = adr.long_name
newAddress.adrStreet = adrStreet
}
if (adr.types.includes('administrative_area_level_1')) {
const adrState = adr.long_name
newAddress.adrState = adrState
}
setAddresss(newAddress)
})
You can try this:
function getAddressAndSaveToState(address) {
address.map(adr => {
const newAddressState = {};
if (adr.types.includes('street_number')) {
const adrStNumber = adr.long_name
newAddressState.adrStNumber = adrStNumber;
}
if (adr.types.includes('route')) {
const adrStreet = adr.long_name
newAddressState.adrStreet = adrStreet;
}
if (adr.types.includes('administrative_area_level_1')) {
const adrState = adr.long_name
newAddressState.adrState = adrState;
}
setAddress({ ...addressState, ...newAddressState });
})
}
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 am trying to update the property of an object which is stored in an array.
my state looks something like this:
state = {
todos: [
{
id: '1',
title: 'first item,
completed: false
},
{
id: '2',
title: 'second item,
completed: false
}
],
}
What I am trying to do is access the second element in the 'todos' array and update the completed property to either false -> true or true -> false.
I have a button with the handler for update, and my class method for the update looks like this:
onUpdate = (id) => {
const { todos } = this.state;
let i = todos.findIndex(todo => todo.id === id);
let status = todos[i].completed
let updatedTodo = {
...todos[i],
completed: !status
}
this.setState({
todos: [
...todos.slice(0, i),
updatedTodo,
...todos.slice(i + 1)
]
});
}
While this does work, I want to find out if there is a more concise way of achieving the same result; I tried to use Object.assign(), but that didn't work out because my 'todos' is an array, not an object. Please enlighten me with better code!
It would be best to use update function to make sure you don't work on outdated data:
onUpdate = (id) => {
this.setState(prevState => {
const copy = [...prevState.todos];
const index = copy.findIndex(t => t.id === id);
copy[index].completed = !copy[index].completed;
return { todos: copy }
})
}
You can simply copy your todos from state, then make edits, and after that put it back to the state
onUpdate = (id) => {
var todos = [...this.state.todos]
var target = todos.find(todo => todo.id == id)
if (target) {
target.completed = !target.completed
this.setState({ todos })
}
}
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.