How do I update deeply nested array with hooks in react? - javascript

I have a nested array of objects, each object have a nested options array like this.
const [formFields, setFormFields ] = useState({
formTitle: '',
fields: [
{name: 'country', val: '', type: 'radio', options: ['Japan', 'Korea', 'usa'] },
{name: 'state', val: '', type: 'select', options: ['texas', 'florida']},
{name: 'location', val: '', type: 'text', options: []},
]})
Each of the items in the nested options array is supposed to be a value in a textInput which is editable.
I want to be able to add/remove/edit these values inside the textInput with a button click.
Please how will I be able to achieve this?
my code
<Containter>
{formFields.fields.map((field, index) => (
<View key={index}>
<View>
<TextInput
onChangeText={(value ) => {
onChange({name: field.name, value });
}}
value={field.name}
/>
</View>
{(field.type === 'select' || field.type === 'radio') && (
<>
{field.options.map((option) => (
<TextInput value={option}
onChangeText={(value ) => {
onChange({name: field.options, ...field.options, value });
}}
/>
<Text onPress={removeOption}>X</Text>
))}
<Button title="add option" />
</>
)
}
<IconButton
icon="delete"
onPress={handleRemoveField}
/>
</View>
))}
<Button
onPress={handleAddField}
title="Add"
/>
</Containter>

Add & remove implementation:
onAdd (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) {
const options = [...field.options,value]
return {...field, options}
}
return field
})
setFormFields(
{
...formFields,
fields
}
)
}
onRemove (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) {
const options = field.options.filter((item) => item != value)
return {...field, options}
}
return field
})
setFormFields(
{
...formFields,
fields
}
)
}

// in constructor
this.onChange = this.onChange.bind(this)
// in class
onChange (index,value) {
this.setState(state => {
const fields = state.fields.map((field,i) => {
if (i==index) field.val = value
return field
})
return {
...state,
fields
}
})
}
// in component
onChangeText( (e) => onChange(index, e.target.value) )

For value changing:
onChange (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) field.val = value
return field
})
setFormFields({
...formFields,
fields
})
}
...
// somewhere in input element
<TextInput ... onChangeText={(e) => onChange(index,e.target.value)} .. />

Related

Formik onChange is not working for dropdown using react-select

Below is the code where I am trying to select value from dropdown.
handleChange is not working, when I select the value from dropdown it's not getting updated with selected value from dropdown. It's getting vanished(blank).
Dropdown values are getting populated when I select it's not catching the new selected value.
Can someone help me on this like what I am missing here?
export const FormikSelectField = ({ label, ...props }) => {
const [field, meta] = useField(props);
const [isFocused, setOnFocus] = useState(false);
const handleChange = (value) => {
// this is going to call setFieldValue and manually update values.topcis
console.log('Value in handlechange..', value ,'and', props.name);
props.onChange(props.name, value.value);
};
const handleFocus = () => {
setOnFocus(true);
};
const handleBlur = (e) => {
// this is going to call setFieldTouched and manually update touched.topcis
setOnFocus(false);
props.onBlur(props.name, true);
field.onBlur(e);
};
return (
<>
<label htmlFor={props.labelName}>{props.labelName} </label>
<Select
id={props.labelName}
options={props.options}
isMulti={false}
onChange={handleChange}
onBlur={(e) => handleBlur(e)}
placeholder='Select an option'
onFocus={handleFocus}
value={props.value}
/>
{meta.touched && meta.error && !isFocused ? (
<div className="error">{meta.error}</div>
) : null}
</>
);
};
formikInitialValues = () => {
return {
Name: [{
title: '',
value: '',
}]
};
};
YupValidationSchema = () => {
return Yup.object({
Name: Yup.array()
.of(
Yup.object().shape({
title: Yup.string().required(),
value: Yup.string().required(),
})
)
.required("Please select an option")
.nullable()
});
};
<FormikSelectField
value={this.state.selectNameOption}
onChange={this.handleNameChange}
onBlur={this.handleNameChangeBlur}
error={formik.errors.Name}
options={this.state.NameOptions}
touched={formik.touched.Name}
name="Name"
labelName="Name"
/>
You should avoid mixing the state when using Formik. Formik will take care of the state for you.
import { Formik, Form, useField, ErrorMessage } from "formik";
import * as Yup from "yup";
import Select from "react-select";
const iceCreamOptions = [
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" }
];
const FormSelect = ({ name, options }) => {
const [field, meta, helpers] = useField(name);
return (
<>
<Select
name={name}
value={field.value}
onChange={(value) => helpers.setValue(value)}
options={options}
onBlur={() => helpers.setTouched(true)}
/>
<ErrorMessage name={name} />
</>
);
};
const initialValues = {
icecream: null
};
const validationSchema = Yup.object().shape({
icecream: Yup.object()
.shape({
value: Yup.string(),
label: Yup.string()
})
.required("Please select a value")
.nullable()
});
export default function App() {
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => console.log(values)}
validationSchema={validationSchema}
>
{(props) => {
return (
<Form>
<pre>{JSON.stringify(props, undefined, 2)}</pre>
<FormSelect name="icecream" options={iceCreamOptions} />
</Form>
);
}}
</Formik>
);
}
Example Working Sandbox
Write this code
onChange={(e:React.ChangeEvent<HTMLInputElement>)=> setFieldValue("title",e.target.value)}
or
onChange={(e)=> setFieldValue("title",e.target.value)}

Save values in array of data

My react application take data from user and should output them when user click on SUBMIT button.
Now user open the application appears a number input. There he can set a number, for example 2. After that appears 2 boxes, where user can add fields cliking on the button +. In each field user have to add his name and last name. Now the application works, but no in the right way.
Now, if user add his user name and last name in first box and click on SUBMIT, everithing appears in:
const onFinish = values => {
const res = {
data: [dataAll]
};
console.log("Received values of form:", res);
};
But if user add one another inputs clicking on add fields button, the first data dissapears. The same issue is when user add data in 2 boxes, the data is saved just from the last box.
function handleInputChange(value) {
const newArray = Array.from({ length: value }, (_, index) => index + 1);
setNr(newArray);
}
const onFinish = values => {
const res = {
data: [dataAll]
};
console.log("Received values of form:", res);
};
return (
<div>
<Form name="outter" onFinish={onFinish}>
{nr.map(i => (
<div>
<p key={i}>{i}</p>
<Child setDataAll={setDataAll} nr={i} />
</div>
))}
<Form.Item
name={["user", "nr"]}
label="Nr"
rules={[{ type: "number", min: 0, max: 7 }]}
>
<InputNumber onChange={handleInputChange} />
</Form.Item>
<Form.Item>
<Button htmlType="submit" type="primary">
Submit
</Button>
</Form.Item>
</Form>
</div>
);
};
Mu expected result after clicking on submit is:
data:[
{
nr:1,
user: [
{
firstName:'John',
lastName: 'Doe'
},
{
firstName:'Bill',
lastName: 'White'
}
]
},
{
nr:2,
user: [
{
firstName:'Carl',
lastName: 'Doe'
},
{
firstName:'Mike',
lastName: 'Green'
},
{
firstName:'Sarah',
lastName: 'Doe'
}
]
},
]
....
// object depend by how many fields user added in each nr
Question: How to achieve the above result?
demo: https://codesandbox.io/s/wandering-wind-3xgm7?file=/src/Main.js:288-1158
Here's an example how you could do it.
As you can see Child became Presentational Component and only shows data.
Rest of logic went to the Parent component.
const { useState, useEffect, Fragment } = React;
const Child = ({nr, user, onAddField, onChange}) => {
return <div>
<div>{nr}</div>
{user.map(({firstName, lastName}, index) => <div key={index}>
<input onChange={({target: {name, value}}) => onChange(nr, index, name, value)} value={firstName} name="firstName" type="text"/>
<input onChange={({target: {name, value}}) => onChange(nr, index, name, value)} value={lastName} name="lastName" type="text"/>
</div>)}
<button onClick={() => onAddField(nr)}>Add Field</button>
</div>
}
const App = () => {
const [items, setItems] = useState([
{
nr: 1,
user: []
},
{
nr: 2,
user: [
{firstName: 'test1', lastName: 'test1'},
{firstName: 'test2', lastName: 'test2'}
]
}
]);
const onCountChange = ({target: {value}}) => {
setItems(items => {
const computedList = Array(Number(value)).fill(0).map((pr, index) => ({
nr: index + 1,
user: []
}))
const merged = computedList.reduce((acc, value) => {
const item = items.find(pr => pr.nr === value.nr) || value;
acc = [...acc, item];
return acc;
}, [])
return merged;
})
}
const onChildChange = (nr, index, name, value) => {
setItems(items => {
const newItems = [...items];
const item = newItems.find(pr => pr.nr === nr);
const field = item.user.find((pr, ind) => index === ind)
field[name] = value;
return newItems;
});
}
const onAddField = (nr) => {
setItems(items => {
const newItems = [...items];
const item = newItems.find(pr => pr.nr === nr);
item.user = [...item.user, {
firstName: '',
lastName: ''
}];
return newItems;
})
}
const onClick = () => {
console.log({data: items});
}
return <div>
{items.map((pr, index) => <Child {...pr} onAddField={onAddField} onChange={onChildChange} key={index} />)}
<input onChange={onCountChange} value={items.length} type="number"/>
<button onClick={onClick}>Submit</button>
</div>
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>

React not re-rendering after array state update

I have a checkbox list UI that is rendered from an array. After I update the array the checkbox list state does not update.
I moved the code where the list is mapped but it does not change the results. The DOM re-render does not happen, see gif below.
I have been looking arround and I see that this issue is already reported however the solution about moving the list.map code out of the function it did not work for me.
Could you suggest me a solution?
What is the source of this problem?
import React,{ useState } from "react"
import
{
Box,
DropButton,
Grid,
Text,
Calendar,
RangeInput,
CheckBox
} from "grommet"
const CalButton = ( { label,onSelect } ) =>
{
return (
<DropButton
label={ label }
dropAlign={ { top: 'bottom',left: 'left' } }
margin="small"
dropContent={
<Box pad="medium" background="ligth-3" elevation="small">
<Calendar range size="medium" onSelect={ onSelect } />
</Box>
} />
)
}
const RangeButton = ( { label,value,onChange,min,max,step,unit,header } ) =>
{
return (
<DropButton
label={ value === null ? label : value + ' ' + unit }
dropAlign={ { top: 'bottom',left: 'left' } }
margin="small"
dropContent={
<Box pad="medium"
background="ligth-3"
elevation="small"
align="center"
>
<Text size="small">{ header }</Text>
<RangeInput
value={ value }
min={ min } max={ max }
onChange={ onChange }
step={ step }
/>
<Text weight="bold">{ value }</Text>
<Text weight="normal">{ unit }</Text>
</Box>
} />
)
}
const FeaturesButton = ( { label,features,onChange } ) =>
{
const FeaturesList = ( { features,onChange } ) => (
<>
{ features.map( ( item,idx ) => (
<CheckBox
key={ item.name }
label={ item.name }
checked={ item.checked }
onChange={ e => onChange( e,idx ) } />)
)
}
</>
)
return (
<DropButton
label={ label }
dropAlign={ { top: 'bottom',left: 'left' } }
margin="small"
dropContent={
<Box pad="medium"
background="ligth-3"
elevation="small"
align="start"
direction="column"
gap="small"
>
<FeaturesList
features={features}
onChange={onChange} />
</Box>
} />
)
}
const destApp = () =>
{
const [ windStrength,setWindStrength ] = useState( null )
const [ windFrequency,setWindFrequency ] = useState( null )
const [ cost,setCost ] = useState( null )
const date = new Date()
const [ month,setMonth ] = useState( date.getMonth() )
const [ listFeatures,setListFeatures ] = useState( [
{
name: 'Butter flat water',
checked: false,
},
{
name: 'Moderately flat water',
checked: false,
},
{
name: '1-2m Waves',
checked: false,
},
{
name: '2m+ Waves',
checked: false,
},
{
name: 'Beginer Friendly',
checked: false,
},
{
name: 'Kite-in-kite-out',
checked: false,
},
{
name: 'Nightlife',
checked: false,
}
] )
const months = [ 'January','February','March','April','May','June','July','August','September','October','November','December' ];
const updateFeaturesList = ( e,idx ) =>
{
listFeatures[ idx ].checked = e.target.checked
const newFeatures = listFeatures
setListFeatures( newFeatures )
console.log( "Updated features list",newFeatures,e.target.checked )
}
return (
<Grid rows={ [ "xsmall","fill" ] }
areas={ [ [ "filterBar" ],[ "results" ] ] }
gap="xxsmall">
<Box gridArea="filterBar"
direction="row-responsive"
gap="xsmall"
pad="xsmall"
justify="center" >
<CalButton label={ months[ month ].toLowerCase() } onSelect={ ( data ) => console.log( data ) } />
<RangeButton
label="wind strength"
header="What's your wind preference?"
min="15"
max="45"
unit="kts"
value={ windStrength }
step={ 1 }
onChange={ ( e ) =>
{
setWindStrength( e.target.value )
console.log( windStrength )
} } />
<RangeButton
label="wind frequency"
header="How often does your destination need to be windy?"
min="1"
max="7"
unit="days/week"
value={ windFrequency }
step={ 1 }
onChange={ ( e ) =>
{
setWindFrequency( e.target.value )
console.log( windFrequency )
} } />
<RangeButton
label="cost"
header="Average daily cost: 1 lunch, diner and doubble room at a midrange hotel?"
min="10"
max="400"
unit="€"
value={ cost }
step={ 1 }
onChange={ ( e ) =>
{
setCost( e.target.value )
console.log( cost )
} } />
<FeaturesButton
label="important features "
features={ listFeatures }
onChange={ updateFeaturesList }
/>
</Box>
<Box gridArea="results"
margin="">
Results go in here!
</Box>
</Grid>
)
}
export default destApp
The problem is in updateFeaturesList, you are mutating the state directly in this line listFeatures[ idx ].checked = e.target.checked, the state reference stay the same and so react does not know if it should re-render.
What you can do is copy the state, before changing it :
const updateFeaturesList = ( e,idx ) =>
{
const newFeatures = [...listFeatures];
newFeatures[ idx ].checked = e.target.checked
setListFeatures( newFeatures )
console.log( "Updated features list",newFeatures,e.target.checked )
}
You're mutating the original state in your updateFeaturesList function. Use the functional form of setState to update your current feature list:
const updateFeaturesList = (e, idx) => {
const { checked } = e.target;
setListFeatures(features => {
return features.map((feature, index) => {
if (id === index) {
feature = { ...feature, checked };
}
return feature;
});
});
};
Also note that calling console.log("Updated features list", newFeatures,e.target.checked) immediately after setting the state won't show the updated state, since setting state is async.
React state will trigger render only if value changed in state.
Modifying or pushing values to array wont change the array reference, here react state uses array reference to decide whether to trigger render or not.so here array is modifying but reference is not changing
solution: copy your array to new array and set to state will solve issue

How to Change value of SelectField get that value and send to server in react native

There are two Select field where am getting data in drop down . Now when I am changing the value and selecting other value its throwing error .
Please suggest me how can I change these value dynamically and submit to server .On button click I have to update this to server . Please help me for that .
class UpdateBillPreferences extends Component {
constructor(props) {
super(props);
const {navigation,clmmasterData} =this.props;
this.state = {
title: 'Update Bill Preferences',
mobile: navigation.state.params.customer.service.serviceNumber,
icon: 'sim',
email:'',
smsNum:'',
faxNum:'',
preferredLanguage: navigation.state.params.customerInfo[0].billingPreferenceDetails.presentationLanguageCode,
preferredCurrency: navigation.state.params.customerInfo[0].billingPreferenceDetails.preferedCurrencyCode,
};
this.presentationLanguageOptions=[{value:'english',label:'English'}, {value:'spanish',label:'Spanish'}]
this.preferredCurrencyOptions=[{value:'dollar',label:'Dollar'}, {value:'niara',label:'Niara'}]
}
componentDidMount() {
}
OnButtonClick = async (preferredLanguage, preferredCurrency,email,smsNum,faxNum) => {
const { OnButtonClick } = this.props;
await OnButtonClick(preferredLanguage, preferredCurrency,email,smsNum,faxNum);
this.setState({
preferredLanguage: '',
preferredCurrency:'',
email :'',
smsNum :'',
faxNum :''
})
}
simRegionChanged = (val, target) => {
this.handleChange({ field: "preferredLanguage" }, target);
};
handleChange = (props, e) => {
let tempObj = this.state.presentationLanguageCode;
tempObj[props.field] = e;
this.setState({ presentationLanguageCode: tempObj });
};
render() {
let { title, mobile, icon } = this.state;
const { navigation,clmmasterData} = this.props;
const {billingAddressDetails,billingPreferenceDetails} = navigation.state.params.customerInfo[0];
const {masterData , language} = clmmasterData;
let submitBtn = { label: 'Submit', OnSubmit: this.onSubmit };
let currencyData=[];
masterData.preferredCurrency.map(({ code: value, name: label }) => {
currencyData.push({ value, label });
});
let languageData=[];
masterData.language.map(({ code: value, name: label }) => {
languageData.push({ value, label });
});
return (
<ImageBackground source={BG} style={styles.imgBG}>
<ScrollView>
<View style={styles.container}>
<View>
<Header title={title} subtitle={mobile} icon={icon} navigation={navigation}/>
</View>
<View style={styles.contentContainer}>
<View style={{ padding: 20 }}>
<Form style={{ width: '100%' }}>
<SelectField
label="Presentation Language"
node="presentationLanguage"
options={languageData}
value={this.state.preferredLanguage}
onChange={this.simRegionChanged}
that={this}
setIcon={true}
img="LANGUAGE"
/>
<SelectField
label="Preferred Currency"
options={currencyData}
value={this.state.preferredCurrency}
node="preferredCurrency"
onChange={this.handleChange}
that={this}
setIcon={true}
img="CURRENCY"
/>
<PrimaryBtn label={'submit'} disabled={false} onPress={()=> this.OnButtonClick(this.state.preferredLanguage,this.state.preferredCurrency,
Thanks
Your problem is here:
simRegionChanged = (val, target) => {
this.handleChange({ field: "preferredLanguage" }, target);
};
handleChange = (props, e) => {
let tempObj = this.state.presentationLanguageCode;
tempObj[props.field] = e;
this.setState({ presentationLanguageCode: tempObj });
};
You are calling handleChange with the prop {field: "preferredLanguage"}. The first line of handleChange then returns undefined because presentationLanguageCode isn't in your state, so when you then do this:
tempObj[props.field] = e; // tempObj["preferredLanguage"] = e
... it throws the error because you're attempting to set a property on an object that doesn't exist.
It looks like you should just be updating preferredLanguage:
handleChange = (props, e) => {
this.setState({ preferredLanguage: e });
};

React Native - Handling dynamic form depending on it's parent answer

I have a simple state using JSObject, and the state look like:
pertanyaan: [{
label: "label1",
type: 'dropdown',
key: 'keyFoo1',
option: [{
value: "foo1"
},
{
value: "foo2",
additional: {
label: 'label1-1',
type: 'date',
key: 'keyFoo1-1',
option:null
}
},
{
value: "foo3",
additional:{
label: 'label1-2',
type: 'dropdown',
key: 'keyFoo1-2',
option:[
{value:"Tidak ada orang"},
{value:"Bertemu tetangga"},
]
}
},
]
},
{
label: "Label2",
type: 'dropdown',
key: 'keyFoo2',
option: [{
value: "Rumah"
},
{
value: "Tempat Kerja"
},
]
}
]
With those JSObject, i want to achieve some form depending on the answer of each parent,
Example: label1 has 3 option ( foo1, foo2, foo3), if the answer of label1 is foo2 i need to render Date Component, and if label1 answering foo3 i need to render Dropdown Component,
with below code I just can rendering label1 with foo2 answer:
renderVisit(){
var renderin = this.state.pertanyaan.map((item, index)=>{
if(this.state[item.key] == undefined){
this.setState({[item.key]:item.option[0].value})
}
let data = item.option.filter((val)=>{return val.value == this.state[item.key]})[0]
return(
<View>
{/*dropdown Component*/}
<View key={index}>
<CustomText>{item.label}</CustomText>
<Picker
mode="dropdown"
selectedValue={this.state[item.key]}
onValueChange={(itemVal)=> this.onChangePicker(item, index, itemVal)}
>
{item.option.map((itemPicker, indexPicker)=>{
return(
<Picker.Item label={itemPicker.value} value={itemPicker.value} key={indexPicker} color={Color.blue_900}/>
)
})}
</Picker>
</View>
{data!=undefined && data.additional!=undefined &&
{/*dateComponent*/}
<View>
<CustomText>{data.additional.label}</CustomText>
<TouchableOpacity onPress={()=>this.openDate(data.additional)}>
<CustomText>{this.state[data.additional.key] == undefined? "Select Date" : new Date(this.state[data.additional.key]).toDateString()}</CustomText>
<MaterialCommunityIcons name="calendar" size={34} />
</TouchableOpacity>
</View>
}
</View>
)
})
return renderin
}
anyone can help me to achieve my goal and makes the code more readable?
This is the way how I would implement the dynamic selection of a dropdown component. You can use the DropDown component recursively by passing the component itself as a child of another dropdown;
const Date = () => 'Date Component';
const Foo = () => 'Foo';
const Bar = () => 'Bar';
class ListItem extends React.Component {
handleClick = () => {
const { option: {id}, onClick } = this.props;
onClick(id);
}
render(){
const { option: { label } } = this.props;
return (
<li onClick={this.handleClick}>{label}</li>
)
}
}
class DropDown extends React.Component {
state = {
selectedOption: null
}
handleOptionClick = optionId => {
const { options } = this.props;
this.setState({ selectedOption: options.find(option => option.id === optionId).child });
}
render(){
const { selectedOption } = this.state;
const { options } = this.props;
return (
<ul>
{options.map(option => (
<ListItem
option={option}
onClick={this.handleOptionClick}
/>
))}
{selectedOption}
</ul>
)
}
}
const DropDownOptions = [
{id: '1', label: 'label-1', child: <Date />},
{id: '2', label: 'label-2', child: <DropDown options={[
{id: '2-1', label: 'label-2-1', child: <Foo />},
{id: '2-2', label: 'label-2-2', child: <Bar />}
]} />}
]
ReactDOM.render(<DropDown options={DropDownOptions} />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Categories