I´m working on a React project, which involves displaying a value (DisplayValue) and then storing that value inside state so that I can use it later. Problem is state is always one step behind (for instance, if displayValue is "12", value is just 1). I need both values to be the same. Is it because setState is async? How can I fix it?
inputDigit(digit) {
const {
pendingOperation,
displayValue
} = this.state;
if (pendingOperation) {
this.setState({
displayValue: String(digit),
pendingOperation: false
})
}
value1 = parseFloat(displayValue);
this.setState({
displayValue: displayValue === "0" ? String(digit) : displayValue + String(digit),
value: value1
}, () => {
console.log(this.state.value)
})
};
Codepen: https://codepen.io/HernanF/pen/jXzPJp
You're breaking a fundamental React rule: Never set state based on existing state by passing an object into setState. Instead, use the callback form, and use the state object the callback form receives. You also probably want to call setState once, not (potentially) twice.
So, you want those changes in the update callback, something like this:
inputDigit(digit) {
this.setState(
({pendingOperation, displayValue}) => {
const newState = {};
if (pendingOperation) {
newState.displayValue = String(digit);
newState.pendingOperation = false;
}
newState.value = parseFloat(displayValue);
// Not sure what you're trying to do with the second setState calls' `displayValue: displayValue === "0" ? String(digit) : displayValue + String(digit),`...
return newState;
},
() => {
console.log(this.state.value)
}
);
}
There seem to be a problem in the code, not React
value1 = parseFloat(displayValue);
should be
value1 = parseFloat(displayValue + String(digit));
The same as for displayValue
Related
I am getting different behaviour depending on whether I am using a boolvalue on with useState, or whether I am using a bool value inside an object with useState.
This first bit of code will show the hidden text when the button is pressed. It uses contextMenuIsOpen which is a bool directly on the state, to control the visibility of the hidden text.
const Parent = () => {
const [contextMenuState, setContextMenuState] = useState({ isOpen: false, x: 0, y: 0, clipboard:null });
const [contextMenuIsOpen, setContextMenuIsOpen] = useState(false);
const openChild = ()=>{
setContextMenuIsOpen(true);
}
return <div><h1>Hello</h1>
<button onClick={openChild}>Open Child</button>
{contextMenuIsOpen &&
<h1>hidden</h1> }
</div>
}
export default Parent;
This next bit of code uses a property on an object which is on the state. It doesn't show the hidden text when I do it this way.
const Parent = () => {
const [contextMenuState, setContextMenuState] = useState({ isOpen: false, x: 0, y: 0, clipboard:null });
const [contextMenuIsOpen, setContextMenuIsOpen] = useState(false);
const openChild = ()=>{
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
return <div><h1>Hello</h1>
<button onClick={openChild}>Open Child</button>
{contextMenuState.isOpen &&
<h1>hidden</h1> }
</div>
}
export default Parent;
React checks objects for equality by checking their reference.
Simply, look at the below example.
const x = { a : 1, b : 2};
x.a = 3;
console.log(x===x);
So when you do the below,
const openChild = ()=>{
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
You are not changing the reference of contextMenuState. Hence, there is no real change of state and setContextMenuState does not lead to any rerender.
Solution:
Create a new reference.
One example, is using spread operator:
const openChild = ()=>{
setContextMenuState({ ...contextMenuState , isOpen : true });
}
The problem with your second approach is that React will not identify that the value has changed.
const openChild = () => {
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
In this code, you refer to the object's field, but the object reference itself does not change. React is only detecting that the contextMenuState refers to the same address as before and from its point of view nothing has changed, so there is no need to rerender anything.
If you change your code like this, a new object will be created and old contextMenuState is not equal with the new contextMenuState as Javascript has created a new object with a new address to the memory (ie. oldContextMenuState !== newContextMenuState).:
const openChild = () => {
setContextMenuState({
...contextMenuState,
isOpen: true
});
}
This way React will identify the state change and will rerender.
State is immutable in react.
you have to use setContextMenuState() to update the state value.
Because you want to update state according to the previous state, it's better to pass in an arrow function in setContextMenuState where prev is the previous state.
const openChild = () =>{
setContextMenuState((prev) => ({...prev, isOpen: true }))
}
Try change
contextMenuState.isOpen = true;
to:
setContextMenuState((i) => ({...i, isOpen: true}) )
never change state like this 'contextMenuState.isOpen = true;'
Please see the code below.
In the return statement, I am trying to add components with the structure:
inputs
inputId
rowId
value
isValid
rowId
value
isValid
inputId
rowId
value
isValid
rowId
value
isValid
However, when I try to use spread syntax to preserve the state and simply add nodes to the state, I cannot load the already-defined rowId components for a specific inputId because syntax such as the following doesn't work ...state.inputs.[action.inputId].rowId.
What is currently happens is that "state" only ever holds one rowId component (the latest one) because it overwrites the previous ones because I cannot correctly use spread syntax to preserve initial state.
Code:
const formReducer = (state, action) => {
switch (action.type) {
case 'INPUT_CHANGE':
let formIsValid = true;
for (const inputId in state.inputs) {
if (inputId === action.inputId) {
formIsValid = formIsValid && action.isValid;
} else {
formIsValid = formIsValid && state.inputs[inputId].isValid;
}
}
return {
...state,
inputs: {
...state.inputs,
[action.inputId]: {
[action.rowId]: { //This is the problematic line ... I would like to execute ' ...state.inputs.[action.inputId].rowId ' before this line
value: action.value,
isValid: action.isValid
}
}
},
isValid: formIsValid
};
default:
return state;
}
};
Your code(logic) really difficult to read.
Anyway, it seems you are miss using object key as variable.
Use :
...state.inputs[action.inputId].rowId
Instead of this :
...state.inputs.[action.inputId].rowId
If I understand correctly you need ...state.inputs[action.inputId]:
return {
...state,
inputs: {
...state.inputs,
[action.inputId]: {
...state.inputs[action.inputId],
[action.rowId]: {
// Another example: if you would also need to merge here with the previous state, you would need:
// ...(state.inputs[action.inputId] ? state.inputs[action.inputId][action.rowId] : [],
// OR if optional chaining operator is supported:
// ...state.inputs[action.inputId]?.[action.inputId],
value: action.value,
isValid: action.isValid
}
}
},
isValid: formIsValid
};
(I hope there's no syntax error).
Or use something like Immer if you want more flexibility/readability when working with immutable data.
Hi i have parrent and child component. Parrent component is receiving data from server. This data are saved in state.data. Now, when i do action in child component, it should be call method from parrent controller. This is working now. Problem is inside this method which i am calling. I am receiving id as parameter. This data in parrent have list of items (packages) and every item has id. I need to update only one of them by id (or other way i don't know right way). Please how i can do it? I need to update isOpen state only that one item i open by clicking on button in child component
My method (but i am not sure if i started to do this right way), i stucked on this problem for while:
changeIsOpenState(typeOfPart: Number, id: Number) {
console.log(this.state.data.packages);
const selectedObject = this.state.data.packages.filter((obj) => {
const val= (obj.id === id) ? obj : false;
return val;
});
}
Array of data i want update (isOpen property).
what about immutably? I think u can use dot-prop-immutable package in this way:
const state = {
packages: [
{ isOpen: false, id: 1 },
{ isOpen: false, id: 2 },
{ isOpen: false, id: 3 }
]
};
const index = state.packages.findIndex(obj => obj.id === 3);
const newState = dotProp.set(state, `packages[${index}].isOpen`, true);
you could do it the ol' way :
changeIsOpenState(typeOfPart: Number, id: Number) {
// Copy the packages so you won't mumtate your state directly
const packages = Object.assign({}, ...this.StaticRange.data.packages);
// Get the package to edit and its index in the packages object
let packageIndex;
let packageToEdit;
for(let i = 0; i <= packages.length; i++){
if(packages[i].id === id){
packageIndex = i;
packageToEdit = packages[i];
packageToEdit.isOpen = true
}
}
packages[packageIndex] = packageToEdit;
setState({...this.state, data:{...this.state.data, packages}});
}
I did it like this:
1.I copy current data to another variable
2.Filter data by id
3.Save array key of item with same id
4. Change cloned value with negation
5. save new data to state
changeIsOpenState(typeOfPart: Number, id: Number) {
const subjectDataCopy = cloneDeep(this.state.data);
const keys = [];
subjectDataCopy.packages.filter((obj, key) => {
if (obj.id === id) {
keys.push(key);
}
});
subjectDataCopy.packages[keys[0]].isOpen = !subjectDataCopy.packages[keys[0]].isOpen;
this.setState({data: subjectDataCopy});
}
If there is better option to do this please let me know :)
Hello i have problem about change state after onClick with this function i dont know why this is doesnt work because console.log displayed difference value and i dont know why i cant set the same state.
`doneUndone = (index) => {
console.log(!this.state.scores[index].done)
const test = !this.state.scores[index].done
this.setState({
scores: test,
})
}`
here will be all code of this aplication https://codepen.io/RetupK/pen/xxKmELd?editors=0010
As per your state scores is an array and in your method of done you are assigning Boolean value to it where as it must be an array itself. Because you're using .map() in your render method which only works with array not boolean.
What you need to do is change the done property of particular object in scores and pass the newly updated scores object to setState method and it will work.
doneUndone = (index) => {
this.state.scores[index].done = !this.state.scores[index].done
this.setState({
scores: this.state.scores,
})
}
If you use this.state to get previously done value you might have problems when you fire doneUndone method multiple times (e.g. clicking button few times in a row). That's why I suggest such solution:
doneUndone = index => {
this.setState(state => ({
scores: state.scores.map((score, idx) =>
idx === index ? { ...score, done: !score.done } : score
)
}));
};
The doneUndone method isn't updating the state properly. You can check the method form here.
doneUndone = (index) => {
const score = this.state.scores[index];
const updatedScore = {...score, done: !score.done};
const updatedScores = [...this.state.scores];
updatedScores[index] = updatedScore;
this.setState({
...this.state,
scores: updatedScores
})
}
doneUndone = (index) => {
let modScores = this.state.scores;
modScores[index].done=!this.state.scores[index].done
this.setState({
scores: modScores
})
}
cleaner way to do it
I have an array of 16 objects which I declare as a state in the constructor:
this.state = {
todos:[...Array(16)].map((_, idx) => {
return {active: false, idx}
}),
}
Their status will get updated through an ajax call in ComponentDidMount.
componentDidMount()
{
var newTodos = this.state.todos;
axios.get('my/url.html')
.then(function(res)
{
newTodos.map((t)=>{
if (something something)
{
t.active = true;
}
else
{
t.active = false;
}
}
this.setState({
todos:newTodos,
})
}
}
and then finally, I render it:
render(){
let todos = this.state.todos.map(t =>{
if(t.active === true){
console.log('true'}
else{
console.log('false')
}
})
return (
<div></div>
)
}
They all appear as active = false in the console, they never go into the if condition. When
I print out the entire state it appears not to be updated in the render method. In the console it says "value below was just updated now".
I thought changes to the state in ComponentWillMount will call the render function again?
How do I make that React will accept the new values of the state?
componentDidMount()
{
var newTodos = []; // <<<<<<<<<<<<<
axios.get('my/url.html')
.then(function(res)
{
newTodos = this.state.todos.map((t)=>{ //<<<<<<<<<<<<<<<
if (something something)
{
t.active = true;
}
else
{
t.active = false;
}
return t; //<<<<<<<<<<<<<<<<<<
} // <<<<< are you missing a semi-colon?
this.setState({
todos:newTodos,
})
}
}
The map() argument (in your code) is a function, not an expression, so an explicit return must be provided. I.E.:
xxx.map( t => ( "return t is implicit" ) );
xxx.map( t => { "return t must be explicit" } );
And, as #DanielKhoroshko points out, your new variable points to this.state. And of course never, never, ever alter this.state directly. Since map() returns a new array, not the original as altered, that's why we use map() and not forEach()
That is because you are actually not providing any new state, but mutating it instead.
React uses shallow comparison be default (where to objects are equal if they reference the same memory address). And that's exactly what's happening here:
var newTodos = this.state.todos; // newTodos === this.state.todos
this.setState({ todos:newTodos }) // replaces two equal addresses, so it simply doesn't change anything
The easiest solution, though probably not the most performant would be to clone your todos array:
var newTodos = [...this.state.todos]; // newTodos !== this.state.todos