How can I modify the first object in a React UseState array? - reactjs

SANDBOX LINK
I am using a FormControl to try to update my state which defaults at const [numberOfChildren, updateNumberOfChildren] = useState([{age: undefined}]); I want to modify the first object in the array when a user clicks a button, and then enters a value in an input. I try to update the age with another useState Currently, the array's first object is {age: undefined} and is not updated by the useState hook
The code looks like this
<FormControl
placeholder="Age"
aria-label="Age"
aria-describedby="basic-addon2"
onChange={async (e) => {
await updateAge(e.target.value);
}}
/>
updated by the button
<Button
className="align-button"
onClick={async (e) => {
e.preventDefault();
if(numberOfChildren.length < 1) {
await updateNumberOfChildren((children) => [
...children
]);
} else {
await updateNumberOfChildren((children) => [
...children,
{ childAge: age },
]);
}
console.log(numberOfChildren)
}}
style={{ width: "100%" }}
variant="outline-primary"
type="submit"
size="lg"
>
Add Child
</Button>{" "}
Here is a sandbox, please have a look at the console for the output SANDBOX

The way you were doing it using const [age, updateAge] = useState(undefined); won't get you what you want because by doing that you'll only update the latest added child so you can't go back to the first one and modify or even modify any previous children after adding them as the current setup has no way to differentiate between which one you're trying to modify.
So, The idea here is that you need to identify each object in the array with something unique so I changed the object structure to the following:
const [numberOfChildren, updateNumberOfChildren] = useState([
{ id: 1, age: undefined }
]);
And here's how you update the state explaining every line:
// Update numberOfChildren state
function updateData(e) {
// Grab the id of the input element and the typed value
const { id, value } = e.target;
// Find the item in the array that has the same id
// Convert the grabed id from string to Number
const itemIndex = numberOfChildren.findIndex(
item => item.id === Number(id)
);
// If the itemIndex is -1 that means it doesn't exist in the array
if (itemIndex !== -1) {
// Make a copy of the state
const children = [...numberOfChildren];
// The child item
const child = children[itemIndex];
// Update the child's age
children.splice(itemIndex, 1, { ...child, age: value });
// Update the state
updateNumberOfChildren(children);
}
}
And when you add a new child the latest added child will have the id of the numberOfChildren state length plus 1 as I used 1 as a starting point:
onClick={e => {
e.preventDefault();
updateNumberOfChildren([
...numberOfChildren,
{ id: numberOfChildren.length + 1, age: undefined }
]);
}}
Finally, If you want to check any state value don't use console.log() after setState() because setState() is async so you won't get the changes immediately and since you're using hooks the only way around this is useEffect():
// Check the values of numberOfChildren whenever they get updated
useEffect(() => {
console.log("numberOfChildren", numberOfChildren);
}, [numberOfChildren]);
Here's the sandbox. Hopefully everything now is crystal clear.

Related

Dynamically create React.Dispatch instances in FunctionComponents

How can I create an array of input elements in react which are being "watched" without triggering the error for using useState outside the body of the FunctionComponent?
if I have the following (untested, simplified example):
interface Foo {
val: string;
setVal: React.Dispatch<React.SetStateAction<string>>;
}
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([])
const addVal = () => {
const [val, setVal] = useState('')
setAllVals(allVals.concat({val, setVal}))
}
return (
<input type="button" value="Add input" onClick={addVal}>
allVals.map(v => <li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>)
)
}
I will get the error Hooks can only be called inside of the body of a function component.
How might I dynamically add "watched" elements in the above code, using FunctionComponents?
Edit
I realise a separate component for each <li> above would be able to solve this problem, but I am attempting to integrate with Microsoft Fluent UI, and so I only have the onRenderItemColumn hook to use, rather than being able to create a separate Component for each list item or row.
Edit 2
in response to Drew Reese's comment: apologies I am new to react and more familiar with Vue and so I am clearly using the wrong terminology (watch, ref, reactive etc). How would I rewrite the code example I provided so that there is:
An add button
Each time the button is pressed, another input element is added.
Each time a new value is entered into the input element, the input element shows the value
There are not excessive or unnecessary re-rendering of the DOM when input elements have their value updated or new input element is added
I have access to all the values in all the input elements. For example, if a separate submit button is pressed I could get an array of all the string values in each input element. In the code I provided, this would be with allVals.map(v => v.val)
const [val, setVal] = useState('') is not allowed. The equivalent effect would be just setting value to a specific index of allVals.
Assuming you're only adding new items to (not removing from) allVals, the following solution would work. This simple snippet just shows you the basic idea, you'll need to adapt to your use case.
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([])
const addVal = () => {
setAllVals(allVals => {
// `i` would be the fixed index of newly added item
// it's captured in closure and would never change
const i = allVals.length
const setVal = (v) => setAllVals(allVals => {
const head = allVals.slice(0, i)
const target = allVals[i]
const tail = allVals.slice(i+1)
const nextTarget = { ...target, val: v }
return head.concat(nextTarget).concat(tail)
})
return allVals.concat({
val: '',
setVal,
})
})
}
return (
<input type="button" value="Add input" onClick={addVal} />
{allVals.map(v =>
<li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>
)}
)
}
React hooks cannot be called in callbacks as this breaks the Rules of Hooks.
From what I've gathered you want to click the button and dynamically add inputs, and then be able to update each input. You can add a new element to the allVals array in the addVal callback, simply use a functional state update to append a new element to the end of the allVals array and return a new array reference. Similarly, in the updateVal callback use a functional state update to map the previous state array to a new array reference, using the index to match the element you want to update.
interface Foo {
val: string;
}
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([]);
const addVal = () => {
setAllVals((allVals) => allVals.concat({ val: "" }));
};
const updateVal = (index: number) => (e: any) => {
setAllVals((allVals) =>
allVals.map((el, i) =>
i === index
? {
...el,
val: e.target.value
}
: el
)
);
};
return (
<>
<input type="button" value="Add input" onClick={addVal} />
{allVals.map((v, i) => (
<li key={i}>
<input value={v.val} onChange={updateVal(i)} />
</li>
))}
</>
);
}

What is the best way to update an object in a usestate and see the change immediately

const TestScreen = (props) => {
const [data, setData] = useState([
{ "id": "0", "user": "Lisa","amount": 1 },
]);
return(
<View style={{
flex:1,
alignItems:'center',
justifyContent:'center'
}}>
<Text>amount : {data[0].amount}</Text>
<Text>User : {data[0].user}</Text>
<Button title="update" onPress={()=>setData(??????)}></Button>
</View>
)
}
export default TestScreen;
what is the best way to add an amount number on the user Lisa? i can do
// setData([{ "id": "0", "user": "Lisa","amount": data[0].amount + 1}])
but what i have 5 users or 20?
even with a returning function nothing gets updated exept console logged witch show me the actual value
let countAddFunc =(getArr)=>{
let arr = getArr
arr[0].amount++
console.log(arr[0].amount);
return(
arr
)
}
<Button title="update" onPress={()=>setData(countAddFunc(data))}></Button>
One of the key concepts in React is Do Not Modify State Directly. This can be tricky sometimes, especially when dealing with nested data like in your example (a number as a property of an object inside an array).
Below, I refactored your code and added comments to help explain. By creating a function to update the state, and passing that function to each child component's props, the child component will be able to update the state by calling the function.
const {useState} = React;
// component dedicated to displaying each item in the array
const Item = (props) => (
<div style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<div>Amount: {props.item.amount}</div>
<div>User: {props.item.user}</div>
<button onClick={() => {
props.updateAmount(props.item.user);
}}>Increment</button>
</div>
);
const ItemList = () => {
const [data, setData] = useState([
{
id: '0',
user: 'Lisa',
amount: 1,
},
{
id: '1',
user: 'Walter',
amount: 3,
},
]);
const updateAmount = (user, changeAmount = 1) => {
// find the index of the item we want
const index = data.findIndex(item => item.user === user);
// return early (do nothing) if it doesn't exist
if (index === -1) return;
const item = data[index];
// don't modify state directly (item is still the same object in the state array)
const updatedItem = {
...item,
amount: item.amount + changeAmount,
};
// again, don't modify state directly: create new array
const updatedArray = [...data];
// insert updated item at the appropriate index
updatedArray[index] = updatedItem;
setData(updatedArray);
};
return (
<ul>
{data.map(item => (
<li key={item.id}>
<Item item={item} updateAmount={updateAmount} />
</li>
))}
</ul>
);
};
ReactDOM.render(<ItemList />, document.getElementById('root'));
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script>
<div id="root"></div>
can you try like this once, pass second param as user id you want to update
<Button title="update" onPress={()=>setData(countAddFunc(data, 0))}></Button>
let countAddFunc =(getArr, id)=>{
const arrCopy = [...getArr]
const user = arrCopy.find(u => u.id === id )
if (user) {
user.amount++
}
return arrCopy
}
actually you are modifying the state directly, and we can not update state directly and getArr is just a refrence to the data in state, so, we created a copy of array, and modified copied array, and then we set this new array into the state, about the code throwing undefined error, add a check, if (user) user.amount++ and make sure id you send onPress={()=>setData(countAddFunc(data, 0))} actually exist
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately. setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
Read the official documentation for more info here
Also, alternatively you can use useRef or useEffect with dependency array if you want the callback to immediately fire and do the changes.

How to set state in parent without triggering re-render of children in React?

I have a React web app that is effectively a ton of Questions. These questions need to be validated/laid-out based on their own state values (ie: must be a number in a number field), as well as on the values of each other. A few examples of the more complex 'validation':
Questions A, B, and C might be required to have non-empty values before allowing a 'save' button.
Question B's allowable range of values might be dependent on the value of question A.
Question C might only show if question A is set to 'true'.
You can imagine many other interactions. The app has hundreds of questions - as such, I have their configuration in a JSON object like this:
{ id: 'version', required: true, label: 'Software Version', xs: 3 },
{
id: 'licenseType', label: 'License Type', xs: 2,
select: {
[DICTIONARY.FREEWARE]: DICTIONARY.FREEWARE,
[DICTIONARY.CENTER_LICENSE]: DICTIONARY.CENTER_LICENSE,
[DICTIONARY.ENTERPRISE_LICENSE]: DICTIONARY.ENTERPRISE_LICENSE
}
},
... etc.
I would then turn this object into actual questions using a map in the FormPage component, the parent of all the questions. Given the need to store these interaction in the closest common parent, I store all of the Question values in a formData state variable object and the FormPage looks like so:
function FormPage(props) {
const [formData, setFormData] = useState(BLANK_REQUEST.asSubmitted);
const handleValueChange = (evt, id) => {
setFormData({ ...formData, [id]: evt.target.value})
}
return <div>
{QUESTIONS_CONFIG.map(qConfig => <Question qConfig={qConfig} value={formData[qConfig.id]} handleValueChange={handleValueChange}/>)}
// other stuff too
</div>
}
The Question component is basically just a glorified material UI textField that has it's value set to props.value and it's onChange set to props.handleValueChange. The rest of the qConfig object and Question component is about layout and irrelevant to the question.
The problem with this approach was that every keypress results in the formData object changing... which results in a re-render of the FormPage component... which then results in a complete re-render/rebuild of all my hundreds of Question components. It technically works, but results performance so slow you could watch your characters show up as you type.
To attempt solve this, I modified Question to hold it's own value in it's own state and we no longer pass formData to it... the Question component looking something like this:
function Question(props) {
const { qConfig, valueChangedListener, defaultValue } = props;
const [value, setValue] = useState(props);
useEffect(() => {
if (qConfig.value && typeof defaultValue !== 'undefined') {
setValue(qConfig.value);
}
}, [qConfig.value])
const handleValueChange = (evt, id) => {
setValue(evt.target.value);
valueChangedListener(evt.target.value, id)
}
return <div style={{ maxWidth: '100%' }}>
<TextField
// various other params unrelated...
value={value ? value : ''}
onChange={(evt) => handleValueChange(evt, qConfig.id)}
>
// code to handle 'select' questions.
</TextField>
</div>
}
Notably, now, when it's value changes, it stores it's own value only lets FormPage know it's value was updated so that FormPage can do some multi-question validation.
To finish this off, on the FormPage I added a callback function:
const processValueChange = (value, id) => {
setFormData({ ...formData, [id]: value })
};
and then kept my useEffect that does cross-question validation based on the formData:
useEffect(() => { // validation is actually bigger than this, but this is a good example
let missingArr = requiredFields.filter(requiredID => !formData[requiredID]);
setDisabledReason(missingArr.length ? "Required fields (" + missingArr.join(", ") + ") must be filled out" : '');
}, [formData, requiredFields]);
the return from FormPage had a minor change to this:
return <div>
{questionConfiguration.map(qConfig =>
<Question
qConfig={qConfig}
valueChangedListener={processValueChange}
/>
</ div>
)
}
Now, my problem is -- ALL of the questions still re-render on every keypress...
I thought that perhaps the function I was passing to the Question component was being re-generated so I tried wrapping processValueChange in a useCallback:
const processValueChange = React.useCallback((value, id) => {
setFormData({ ...formData, [id]: value })
}
},[]);
but that didn't help.
My guess is that even though formData (a state object on the FormPage) is not used in the return... its modification is still triggering a full re-render every time.
But I need to store the value of the children so I can do some stuff with those values.
... but if I store the value of the children in the parent state, it re-renders everything and is unacceptbaly slow.
I do not know how to solve this? Help?
How would a functional component store all the values of its children (for validation, layout, etc)... without triggering a re-render on every modification of said data? (I'd only want a re-render if the validation/layout function found something that needed changing)
EDIT:
Minimal sandbox: https://codesandbox.io/s/inspiring-ritchie-b0yki
I have a console.log in the Question component so we can see when they render.

My component is mutating its props when it shouldn't be

I have a component that grabs an array out of a prop from the parent and then sets it to a state. I then modify this array with the intent on sending a modified version of the prop back up to the parent.
I'm confused because as I modify the state in the app, I console log out the prop object and it's being modified simultaneously despite never being touched by the function.
Here's a simplified version of the code:
import React, { useEffect, useState } from 'react';
const ExampleComponent = ({ propObj }) => {
const [stateArr, setStateArr] = useState([{}]);
useEffect(() => {
setStateArr(propObj.arr);
}, [propObj]);
const handleStateArrChange = (e) => {
const updatedStateArr = [...stateArr];
updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value);
setStateArr(updatedStateArr);
}
console.log(stateArr, propObj.arr);
return (
<ul>
{stateArr.map((stateArrItem, index) => {
return (
<li key={`${stateArrItem._id}~${index}`}>
<label htmlFor={`${stateArrItem.name}~name`}>{stateArrItem.name}</label>
<input
name={`${stateArrItem.name}~name`}
id={`${stateArrItem._id}~input`}
type="number"
value={stateArrItem.keyValue}
data-index={index}
onChange={handleStateArrChange} />
</li>
)
})}
</ul>
);
};
export default ExampleComponent;
As far as I understand, propObj should never change based on this code. Somehow though, it's mirroring the component's stateArr updates. Feel like I've gone crazy.
propObj|stateArr in state is updated correctly and returns new array references, but you have neglected to also copy the elements you are updating. updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value); is a state mutation. Remember, each element is also a reference back to the original elements.
Use a functional state update and map the current state to the next state. When the index matches, also copy the element into a new object and update the property desired.
const handleStateArrChange = (e) => {
const { dataset: { index }, value } = e.target;
setStateArr(stateArr => stateArr.map((el, i) => index === i ? {
...el,
keyValue: value,
} : el));
}

How to update a Textfield that has a value from an array of objects state variable in React?

In react I am trying to update a rendered mapped array of objects in Textfields with the value set to the objects value and then also be able to update/change the value in the Textfield and corresponding state value. Currently the array of objects is correctly being mapped and displayed with the objects value, however when trying to change the value within the TextField, nothing changes in the display and the console logs result in only changing the last letter of the value. It seems as though because I need the Textfield to start with a value, that value keeps the old value due to the data being rendered with a map or whether I am updating an array of objects wrong? Although in this example an array of objects is not needed, it applies to my component that does need it.
The following is some example code to demonstrate:
import React, { useState} from 'react';
const Test = () => {
const [data, setData] = useState([
{
num: 1,
name: 'hello'
},
{
num: 2,
name: 'world'
},
{
num: 3,
name: 'test'
},
]);
const handleChange = e => {
const { name, value, id } = e.target;
setData(data[id].name = value)
console.log(value)
}
return (
<div>
{
data.map((_itm, index) => (
<TextField key={index} value={_itm.name} onChange={handleChange} name='name' id={index.toString()}/>
))
}
</div>
)
}
So 3 textfields will be displayed with the values, hello, world, and test. When trying to edit the textfields, the values do not change.
Any and all help is appreciated, thank you.
In the state hook, data is set to be an array. So you should always pass in an updated copy of that array whenever calling setData.
const handleChange = e => {
const { value, id } = e.target;
// Make a shallow copy of the current `data`.
const newArray = [...data];
// Update the changed item.
newArray[id] = {
...newArray[id],
name: value
}
// Call setData to update.
setData(newArray);
console.log(value);
}
I had the same problem and the code above didn't work. I did it a little differently and wrote a code that works 100%, no matter how you name the key in the object
const changeHandler = (e) => {
const { id, name, value } = e.target
const newArray = [...data]
newArray[id][name] = value
setForm(newArray)
}

Resources