React useState not updating mapped content - reactjs

I feel like im missing something that is staring me right in the face. I am trying to have content stored in an array of objects update when a checkbox is on or off. The console log is showing the object data is updating correctly so I assume my fault resides in not understanding useState fully?
const [statistics, setStatistics] = useState([
{
id: 1,
content: <div>Content1</div>,
state: true,
},
{
id: 2,
content: <div>Content2</div>,
state: true,
},
]);
In the component:
{statistics.map((item) => (item.state ? item.content : <></>))}
<input
type="checkbox"
onChange={(e) => {
let newArr = statistics;
e.target.checked
? (newArr[0].state = true)
: (newArr[0].state = false);
setStatistics(newArr);
console.log(statistics);
}}
/>

You are trying to change the state directly, instead you need to work with a copy of the state and make all changes to it.
Just replace in your code this string:
let newArr = statistics; // as link to base array
to
let newArr = [...statistics]; // as new copy of base array
and it will works.
React skips all state changes if they are made directly.

To create a new array as copy/clone of another array, in ES6, we can use the spread operator. You can not use = here, since it will only copy the reference to the original array and not create a new variable. Just read here for reference.
In your case, your newArray will refer to the old statistics and will not be detected as the new state. That is why no re-render takes place after you made changes to it.
So here, you can do this:
return (
<>
{statistics.map((item) => (item.state ? item.content : <></>))}
<input
type="checkbox"
onChange={(e) => {
setStatistics((prevStats) => {
const newStats = [...prevStats];
e.target.checked
? (newStats[0].state = true)
: (newStats[0].state = false);
return newStats;
});
}}
/>
</>
);

Related

React - problem updating an array of objects using hooks

I have tried a bunch of approaches but on this, but it's still bugging the hell out of me. I am using React 17.0.1 in this project.
I have a array of objects formatted like so:
gameNumberFields: [{ name: 'gameNum', placeholder: 'GGG', size: '3', value: '', dataindex: '0' }];
For now, there is just one object in the array, but there is always the possibility of more down the road (hence why it's an array).
In the code - this field is pre-populated on initialization - so the "value" of the first index in the array might be something like "123". I use initialState to make this happen:
const [gameNumberFields, setGameNumberFields] = useState(scratchTicketFields?.gameNumberFields ?? []);
When the display is shown to the user - this value is shown to the user in an field using the defaultValue.
return gameNumberFields.map((field, index) => {
const ref = React.createRef();
elemRefs.push(ref);
return (
<div className='d-inline-block ticket-number-inputs' key={`game-numbers--${index}`}>
<input
type='text'
id={field.name}
data-index={field.dataindex}
ref={ref}
className='theme-input'
placeholder={field.placeholder}
size={field.size}
maxLength={field.size}
defaultValue={field.value}
onBlur={(event) => handleGameNumberChange(event, field)}
onKeyUp={(event) => autoTab(event, elemRefs)}
required
/>
<span className='dash'>—</span>
</div>
);
});
}; // end GameIdFieldInputs
So far - so good. The problem I am having is in the onBlur event handler. For some reason - when the user changes the value to something else - it always goes back to the old value.
Here is the handler:
const handleGameNumberChange = async (event, field) => {
// get gameNum from the event target
const gameNum = event.target.value; // say this becomes 999
// do a deep copy of the gameNumberField state.
let gameIdField = JSON.parse(JSON.stringify(gameNumberFields));
// check that we are changing the right index in the array
const fieldIndex = gameIdField.findIndex((obj) => obj.name == field.name);
// make a new object changing the value to 999
let newGameObject = { ...gameIdField[fieldIndex], value: gameNum };
console.log('newGameObject', newGameObject);
//NOTE: At this point the newGameObject is correct and updated with the NEW gameNum of 999
// create a new array and PUSH the new game object onto it
let newGameIdArray = new Array();
newGameIdArray.push(newGameObject);
// Once pushed the array has the OLD game number in it . . . so 123 - WHY?!?!
console.log('newGameObjectArray', newGameIdArray);
setGameNumberFields(newGameIdArray); // updates with the 123 game number. . .
}; // end handleGameNumberChange
So in the method, I deep copy the gameNumberFields into a mutable object. I then update the object with the new gameNumber (from 123 to 999) and all works when I print it out with my console.log for newGameObject.
As soon as I push this object in the new Array - it changes back to 123! Can anyone see a flaw in my code here?
When I finally do call setGameNumberFields - it does set the state (I have a useEffect that prints out the values) but again, its always the OLD values.
Any help is welcome and appreciated!

React.useState Hook only displaying length of object, not object itself

Basically, I have a component named Upload:
const Upload = () => {
const [scene , setScene] = React.useState([]);
// handleUpload = (event) => {
// console.log('Success!');
// }
function renderHandler(s){
console.log('[RENDER] calling...')
console.log(s);
if(scene.length == 0){
console.log('[RENDER] Denied!')
return(
<div>
Nothing is rendered..
</div>
)
} else {
console.log('[RENDER] Working...')
console.log('[RENDER] Received planes:');
console.log(blockState);
console.log(scene);
return (
<View top={blockState}/>
)
}
}
function parseNBT(input) {
setScene(scene.push('1'));
setScene(scene.push('2'));
console.log('scene:');
console.log(typeof scene);
console.log(scene);
console.log('\n+blockState:');
console.log(typeof blockState);
console.log(blockState)
}
return (
<Container>
Input NBT file <br/>
<input type="file" onChange={handleChange}></input>
{renderHandler(scene)}
</Container>
)
}
The issue here is, when I'm setting the scene's state in parseNBT, and console log scene, it gives me the array:
However, when I call it from renderHandler, it simply returns the length of the array, in this case it was 2
Very weird, maybe i'm missing something?
The .push returns the length of the array.
Return value
The new length property of the object upon which the method was called.
Try
setScene( currentScene => [...currentScene, '1'] );
setScene( currentScene => [...currentScene, '2'] );
To summerize briefly, you are treating 'scene' as a mutable object, when it is immutable. Meaning, when you are trying to do a 'scene.push' it is trying to modify an immutable object. A regular array is mutable, but not a react state array. Therefore, you do not want to give an update to scene directly, you want to take its previous state, concatenate it with your new desired value, then make that new value your new state.
Like so:
Replace your lines:
setScene(scene.push('1'));
setScene(scene.push('2'));
with:
setScene((scene) => [...scene, 1]);
setScene((scene) => [...scene, 2]);

Updating nested useState seems to modify the original data

So I have an implementation of a Text Field input alongside a table in which I'm trying to update the state of staged Data before I submit the data to an API.
In the Dialogs parent component, I have the data defined which I want to show in a table as the original state.
The current problem I'm having is the inputted data is somehow updating the original data's state even though I'm not directly touching this data.
Below is a reproduction of it on Codesandbox, So when you open the link typing into the edit value field should not update the current stock field and I don't see why it is.
CodeSandBox
Here is the callback that modifies the state:
const handleUpdateDip = (value, tank) => {
const newData = stagedData;
const foundIndex = newData.dips.findIndex((d) => d.tank === tank);
if (foundIndex !== -1) {
newData.dips[foundIndex].currentStockValue = Number(value);
setStage({
...stagedData,
dips: newData.dips
});
}
};
So yeah this one seems weird to me and I've been banging my head against the keyboard trying to understand whats going on with it since last night so any help would be appreciated!
You are mutating the current object. Try this
setStage((stage) => {
const foundIndex = stage.dips.findIndex((d) => d.tank === tank);
return {
...stage,
dips: stage.dips.map((d, index) => {
if (foundIndex === index) {
return { ...d, currentStockValue: Number(value) };
}
return d;
})
};
});
Instead of this
const foundIndex = stagedData.dips.findIndex((d) => d.tank === tank);
if (foundIndex !== -1) {
stagedData.dips[foundIndex].currentStockValue = Number(value);
setStage({
...stagedData,
dips: stagedData.dips
});
}
I don't see why it's shouldn't update while the code tells it to do so! This line inside handleUpdateDip():
stagedData.dips[foundIndex].currentStockValue = Number(value);
You shouldn't directly mutate the state. You should make a copy of it first change whatever you want and then set the state to the new value e.g.:
const handleUpdateDip = (value, tank) => {
const foundIndex = stagedData.dips.findIndex((d) => d.tank === tank);
if (foundIndex !== -1) {
const newStagedData = { ...stagedData };
newStagedData.dips[foundIndex].currentStockValue = Number(value);
setStage(newStagedData);
}
};
stagedData.dips[foundIndex].currentStockValue = Number(value); this line updates the value of currentStockValue which is used in the "Current Stock" column.
It seems like the table cell left of the input field simply uses the same state that is changed in handleUpdateDip
<TableCell align="right" padding="none">
{row.currentStockValue}
</TableCell>
<TableCell align="right" padding="none">
<InputTextField
id="new-dip"
type="number"
inputProps={{
min: 0,
style: { textAlign: "right" }
}}
defaultValue={row.currentStockValue}
onChange={(event) =>
handleUpdateDip(event.target.value, row.tank)
}
/>
both are currentStockValue, which handleUpdateDips changes in this line
stagedData.dips[foundIndex].currentStockValue = Number(value);
I think I know what you're thinking. You think that on the one hand, you're updating your state in handleUpdateDip(event.target.value, row.tank) with setStage({...}), so you're only changing your state stagedData.
You value for the "Current Stock", however, is mapped to your data variable and not to stagedData.
So in the end your question is: Why ist data changing when you're only manipulating stagedData.
Of course it happens here: const [stagedData, setStage] = useState(() => data);
(btw you don't need to use a function here, const [stagedData, setStage] = useState(data); is fine). You pass in data by reference here, when your setState hits, the reference will be updated and so will your data.
(another BTW: don't call your state variable settings functions simply setState, this is something used by class components in React. Call them like the state you want to set, e.g. setStagedData).
Now, you can elimate this reference, since you only want the initial values anyways. You could do this by passing a copy, like this: const [stagedData, setStagedData] = useState({...data}); But this still won't work - I not really sure why because I don't know enough about the inner workings of useState, but the reason probably is because it's only a shallow copy instead of a deep copy (you can read more about this here).
But if we do a deep copy and pass this in, it works and your original data will stay untouched. You can deep copy by basically stringifying and then parsing it again (which will not copy any methods the object has, just as a warning).
const copy = JSON.parse(JSON.stringify(data));
const [stagedData, setStagedData] = useState(copy);
And just like that your current stock will stay the same:
I forked your CodeSandBox, so you can see it for yourself.

Append array of values to the current array in a state in React JS

I have the state values as
this.state = {
emp: [
{id: "1", name: "A"}
{id: "2", name: "B"}
{id: "3", name: "B"}
]
}
How can I add an array like var arr = {id:"4", name:"D"} to the state emp without removing the current values of array. I just want to append the new array of values to the current state array. Can anyone help?
In modern JavaScript you can use the spread operator:
Add a single item
addItem = item => {
this.setState({
emp: [
...this.state.emp,
item
]
})
}
Add multiple items:
addItems = items => {
this.setState({
emp: [
...this.state.emp,
...items
]
})
}
The spread operator places all the elements in this.state.emp in a new array instance and item gets appended as the last element.
You should not mutate a component's state with other means than setState as your rendered data will get out of sync.
just use concat
this.setState({ emp: this.state.emp.concat('new value') })
The reasons why concat is better than push, unshift are
Array.push
Array.prototype.push allows us to push elements to the end of an
array. This method does not return a new copy, rather mutates the
original array by adding a new element and returns the new length
property of the object upon which the method was called.
Array.unshift
To add elements to the very beginning of an array. Just as push, unshift does not return a new copy of the modified array, rather the new length of the array
Both the ways changes the mutation state of an array. A mutation term is meant to be unchanged because it is our original source.
array.concat
The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
However, You Object.assign() too, that creates a deep copy of object assigned to it.
let emp = Object.assign([],this.state.emp); //type of an array
result
You need to update if using functional setState(since you are updating state based on prevState) and spread syntax like
this.setState(prevState => ({
emp: [
...prevState.emp,
{id:"4",name:"c"}
]
}))
If using a functional component, voila a simple example:
const [hiddenDivs, setHiddenDivs] = useState([1, 2, 3, 4]);
const handleCatDivTitleClick = (divNum: number) => {
if (hiddenDivs.includes(divNum)) {
setHiddenDivs(hiddenDivs.filter((d) => d !== divNum)); //REMOVE FROM ARRAY
} else {
setHiddenDivs([...hiddenDivs, divNum]); //ADD TO ARRAY
}
};
<div class={`${style.catDiv} ${hiddenDivs.includes(1) ? style.catDivHide : ''}`}>
<div class={style.catTitle} onClick={() => handleCatDivTitleClick(1)}>
Imagine a list of categories like this. All begin "hidden" (shrunk-up).
</div>
</div>
<div class={`${style.catDiv} ${hiddenDivs.includes(2) ? style.catDivHide : ''}`}>
<div class={style.catTitle} onClick={() => handleCatDivTitleClick(2)}>
You want to shrink/expand category based on clicking title.
</div>
</div>
<div class={`${style.catDiv} ${hiddenDivs.includes(3) ? style.catDivHide : ''}`}>
<div class={style.catTitle} onClick={() => handleCatDivTitleClick(3)}>
Basically, a home-rolled accordian-type display.
</div>
</div>

React form validation still adds values

So I have a little bit of form validation going on and I am running into an issue. When I first load the web app up and try adding a value and submitting with my button it doesn't allow me and gives me the error I want to see. However, when I add a value setState occurs and then my value is pushed to UI and I try to add another blank value it works and my conditional logic of checking for an empty string before doesn't not go through what am I doing wrong?
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
}, () => {
document.getElementById('test').value = '';
})
console.log(this.state.id);
}
}
You are checking this.state.input but no where in that code are you setting the input value on the state.
Try adding this where it makes sense in your application:
this.setState({ input: 'some value' });
Also, I recommend you use the state to define the application UI. So instead of using document.getElementById('error') or document.getElementById('test').value, have the UI reflect what you have in your state.
See here for more info: https://reactjs.org/docs/forms.html
Instead of manipulating the DOM directly:
document.getElementById('test').value = '';
you'll want to use React:
this.setState({ input: '' });
A good ground rule for React is to not manipulate the DOM directly through calls like element.value = value or element.style.color = 'red'. This is what React (& setState) is for. Read more about this on reactjs.org.
Before you look for the solution of your issue, I noticed that you are directly updating the DOM
Examples
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
document.getElementById('test').value = '';
Unless you have special use case or dealing with external plugins this isn't recommended, when dealing with React you should update using the virtual DOM. https://www.codecademy.com/articles/react-virtual-dom
Pseudo code sample
constructor(props) {
this.state = {
// retain previous states in here removed for example simplicity
errorString: ''
}
}
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
this.setState({
errorString: 'Please enter something first'
});
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
input: '',
});
}
}
// notice the "error" and "test" id this could be omitted I just added this for your reference since you mentioned those in your example.
render() {
return (
<div>
{(this.state.errorString !== '') ? <div id="error" style={{color: 'red'}}>{this.state.errorString}</div> : null}
<input id="test" value={this.state.input} />
</div>
}
Every time you invoke setState React will call render with the updated state this is the summary of what is happening but there are lot of things going behind setState including the involvement of Virtual DOM.

Resources