React JS - How to set state of variable inside variable? - reactjs

I want to set state of this form :
this.state = {
filter: {
search: null,
brands: null,
price: null
}
}
How to set value for search / brands / price ?

Do the following:
this.setState({
filter: {
search: 'value',
brands: 'value',
price: 'value'
}
})

The key is that you don't want to ever mutate a value in state. As a result, you must copy the filter object before passing it to setState. Example:
onSearchChange(value) {
this.setState((state) => {
return {
filter: {
...state.filter,
search: value
}
})
}
Note that I am passing a function to setState. Since the next value of state relies on the previous value, you want to use an updater functions, as the setState docs recommend.
In general, it is nicer if you can keep your state flat. So rather than having a filter object in state, your shape could just be
this.state = {
search: null,
brands: null,
price: null,
}
In which case the above onSearchChange function would just be
onSearchChange(value) {
this.setState({search: value})
}
Definitely a lot easier on the eyes.

I recommend avoiding nested objects and keeping your state flat. e.g.
this.state = {
brandsFilter: null,
priceFilter: null,
searchFilter: null,
};
Component state in react is really nothing more than simple key-value pairs; that's what setState supports. Sure you can have nested objects if you really want. But as the other answers demonstrate, supporting such an approach can be needlessly complex.

you should use the setState function, you can set the filter with updated data like so
const newFilter = {...};
this.setState({filter: newFilter});

You should avoid to mutate React state directly, there are some functions can do immutable jobs for you (ex Object.assign):
const currentFilter = this.state.filter;
const newFilter = Object.assign({}, currentFilter, {search: "new search", brands: [], price: 100});
this.setState({filter: newFilter});
ES 6:
const currentFilter = this.state.filter;
this.setState({
filter: {
...currentFilter,
search: "new search",
brands: []
}
});

this.setState({
filter: {
...this.state.filter,
search: "new search",
brands: 'value',
price: 'value'
}
});
You may try this with Spread Operator.
If you need to preserve the previous filter value.
other wise you can,
this.setState({
filter: {
search: "new search",
brands: 'value',
price: 'value'
}
});

let newfilter = Object.assign({}, this.state.filter)
newfilter.search ="value";
newfilter.brands ="value";
newfilter.price ="value";
this.setState({
filter:newfilter
})

You can access the search, brands, price by using:
this.setState({
filter.search = true,
filter.brands = 'stackoverflow',
filter.price = 1400
})
and to access it, just like usual state access (this.state.filter.search).

Related

How do I change the state of an array of objects in React?

I'm trying to change the state of an array containing an object whenever something is typed inside of an input. The state of the array I'm trying to change looks like this:
const defaultCV = {
personalInfo: {
firstName: "",
lastName: "",
title: "",
about: "",
},
education: [
{
id: uniqid(),
university: "",
city: "",
degree: "",
subject: "",
from: "",
to: "",
},
],
Specifically, I want to change the state of the 'education' section. My current, non-working code looks like this:
const handleEducationChange = (e) => {
setCV((prevState) => ({
...prevState,
education: [
{
...prevState.education,
[e.target.id]: e.target.value,
},
],
}));
};
When I type in the input and the function is triggered, I get the error "Warning: Each child in a list should have a unique "key" prop." I've been trying to make this work for the past few hours, any help as to what I'm doing wrong would be appreciated.
Are you using the Array.map() method to render a list of components? That is a common cause of that error. For example if you are mapping the education array.
You can fix by using the object id as the key since that is already generated for each object:
defaultCV.education.map(institution => {
return <Component key={institution.id} institution={institution} />
}
You are destructuring an array in to an object that will not work
education: [{ // this is the object you are trying to restructure into
...prevState.education, // this here is an array in your state
[e.target.id]: e.target.value,
}, ],
}));
Suppose you render this input field:
<input id='0' type='text' name='university' value={props.value} />
Your event.target object will include these props:
{id = '0', name = 'university', value = 'some input string'}
When updating the state, you have to first find the array item (object), that has this id prop, then you can change its 'name' property and return the new state object.
This worked for me:
setCV(prevState => {
const eduObjIdx = prevState.education.findIndex(obj => obj.id === +e.target.id)
prevState.education[eduObjIdx][e.target.name] = e.target.value
return {
...prevState,
education: [
...prevState.education.splice(0, eduObjIdx),
prevState.education[eduObjIdx],
...prevState.education.splice(eduObjIdx + 1),
],
}
})
Make sure you send the current state of the input value when rendering the component:
<Component value={state.education[currentId].id} />
where currentId is the id of the university you are rendering.
If you render it mapping an array, don't forget the key (answered at 0), otherwise you'll get the above error message.
This way you don't mutate the whole education array. May not be the best solution though.

ReactJS - Proper way for using immutability-helper in reducer

I have the following object which is my initial state in my reducer:
const INITIAL_STATE = {
campaign_dates: {
dt_start: '',
dt_end: '',
},
campaign_target: {
target_number: '',
gender: '',
age_level: {
age_start: '',
age_end: '',
},
interest_area: [],
geolocation: {},
},
campaign_products: {
survey: {
name: 'Survey',
id_product: 1,
quantity: 0,
price: 125.0,
surveys: {},
},
reward: {
name: 'Reward',
id_product: 2,
quantity: 0,
price: 125.0,
rewards: {},
},
},
}
And my reducer is listening for an action to add a reward to my object of rewards:
case ADD_REWARD:
return {
...state, campaign_products: {
...state.campaign_products,
reward: {
...state.campaign_products.reward,
rewards: {
...state.campaign_products.reward.rewards,
question: action.payload
}
}
}
}
So far so good (despite the fact that every object added is named "question")... its working but its quite messy. I've tried to replace the reducer above using the immutability-helper, to something like this but the newObh is being added to the root of my state
case ADD_REWARD:
const newObj = update(state.campaign_products.reward.rewards, { $merge: action.payload });
return { ...state, newObj }
return { ...state, newObj }
First, you must understand how the object shorthand works. If you're familiar with the syntax before ES2015, the above code translates to:
return Object.assign({}, state, {
newObj: newObj
});
Note how the newObj becomes a key and a value at the same time, which is probably not what you want.
I assume the mentioned immutability-helper is this library: https://www.npmjs.com/package/immutability-helper. Given the documentation, it returns a copy of the state with updated property based on the second argument.
You're using it on a deep property so that it will return a new value for that deep property. Therefore you still have to merge it in the state, so you have to keep the approach you've labelled as messy.
What you want instead is something like:
const nextState = update(state, {
$merge: {
campaign_products: {
reward: {
rewards: action.payload
}
}
}
});
return nextState;
Note how the first argument is the current state object, and $merge object is a whole object structure where you want to update the property. The return value of update is state with updated values based on the second argument, i.e. the next state.
Side note: Working with deep state structure is difficult, as you've discovered. I suggest you look into normalizing the state shape. If applicable, you can also split the reducers into sub-trees which are responsible only for the part of the state, so the state updates are smaller.

React.useState is changing initialValues const

I'm experiencing some odd behavior with react's useState hook. I would like to know why this is happening. I can see a few ways to sidestep this behavior, but want to know whats going on.
I am initializing the state with the following const:
const initialValues = {
order_id: '',
postal_code: '',
products: [
{
number: '',
qty: ''
}
]
}
const App = (props) => {
const [values, setValues] = React.useState(initialValues);
...
products is an array of variable size. As the user fills in fields more appear.
The change handler is:
const handleProductChange = (key) => (field) => (e) => {
if (e.target.value >= 0 || e.target.value == '') {
let products = values.products;
products[key][field] = e.target.value;
setValues({ ...values, products });
}
}
What I am noticing is that if I console log initialValues, the products change when the fields are changed. None of the other fields change, only inside the array.
Here is a codepen of a working example.
How is this possible? If you look at the full codepen, you'll see that initialValues is only referenced when setting default state, and resetting it. So I don't understand why it would be trying to update that variable at all. In addition, its a const declared outside of the component, so shouldn't that not work anyway?
I attempted the following with the same result:
const initialProducts = [
{
number: '',
qty: ''
}
];
const initialValues = {
order_id: '',
postal_code: '',
products: initialProducts
}
In this case, both consts were modified.
Any insight would be appreciated.
Alongside exploding state into multiple of 1 level deep you may inline your initial:
= useState({ ... });
or wrap it into function
function getInitial() {
return {
....
};
}
// ...
= useState(getInitial());
Both approaches will give you brand new object on each call so you will be safe.
Anyway you are responsible to decide if you need 2+ level nested state. Say I see it legit to have someone's information to be object with address been object as well(2nd level deep). Splitting state into targetPersonAddress, sourePersonAddress and whoEverElsePersonAddress just to avoid nesting looks like affecting readability to me.
This would be a good candidate for a custom hook. Let's call it usePureState() and allow it to be used the same as useState() except the dispatcher can accept nested objects which will immutably update the state. To implement it, we'll use useReducer() instead of useState():
const pureReduce = (oldState, newState) => (
oldState instanceof Object
? Object.assign(
Array.isArray(oldState) ? [...oldState] : { ...oldState },
...Object.keys(newState).map(
key => ({ [key]: pureReduce(oldState[key], newState[key]) })
)
)
: newState
);
const usePureState = initialState => (
React.useReducer(pureReduce, initialState)
);
Then the usage would be:
const [values, setValues] = usePureState(initialValues);
...
const handleProductChange = key => field => event => {
if (event.target.value >= 0 || event.target.value === '') {
setValues({
products: { [key]: { [field]: event.target.value } }
});
}
};
Probably the simplest move forward is to create a new useState for products which I had started to suspect before asking the question, but a solution to keep the logic similar to how it was before would be:
let products = values.products.map(product => ({...product}));
to create a completely new array as well as new nested objects.
As #PatrickRoberts pointed out, the products variable was not correctly creating a new array, but was continuing to point to the array reference in state, which is why it was being modified.
More explanation on the underlying reason initialValues was changed: Is JavaScript a pass-by-reference or pass-by-value language?

how to map to update an objects with setState in Reactjs

I have this state:
state = {
formdata:{
name: null,
about: null,
price: null,
offerPrice:null,
playStoreUrl:null,
appStoreUrl:null ,
photo:null,
}
}
what I want: update form inside modal i used it to update products. I used new props inside componentWillReceiveProps
I did:
componentWillReceiveProps(nextProps){
let Updateproduct = nextProps.productlist.productlist.Products;
Updateproduct.map((item,i) => {
let formdata = Object.assign({}, this.state.formdata);
formdata.name = item.name
formdata.about = item.about
formdata.price = item.price
formdata.offerPrice = item.offerPrice
formdata.playStoreUrl = item.playStoreUrl
formdata.appStoreUrl = item.appStoreUrl
formdata.photo = item.photo
console.log(formdata)
this.setState({formdata})
})
}
MyProblem: this filled the objects but in the form inside modal only I saw the last product not all in modal when click to update any product it. Note:Updateproduct contains:
{
about: "about product1"
appStoreUrl: "https://itunes.apple.com/us/app/snapchat/id447188370?mt=8"
name: "p1"
offerPrice: 99.99
photo: "images/products/"
playStoreUrl: "images/products/"
price: 1000
}
{
about: "about product2"
appStoreUrl: "https://itunes.apple.com/us/app/snapchat/id447188370?mt=8"
name: "p2"
offerPrice: 99.99
photo: "images/products/"
playStoreUrl: "images/products/"
price: 2000
}
Issue is, you want to store single specific product item clicked by user in state variable, but with current code you are always storing the last product item. Also setState in loop is not a good way.
To solve the issue, store the clicked product detail in state inside onClick handler function only, instead of componentWillReceiveProps method. For that you need to bind the product id with onClick function.
Like this:
onClick={this.getProductId.bind(this, item.id)}
// click handler function
getProductId = (id) => {
let productObj = {};
let Updateproduct = this.props.productlist.productlist.Products;
Updateproduct.forEach((item,i) => {
if(item.id == id) {
productObj = {
name: item.name,
about: item.about,
price: item.price,
offerPrice: item.offerPrice,
playStoreUrl: item.playStoreUrl,
appStoreUrl: item.appStoreUrl,
photo: item.photo
};
this.setState({ formdata: productObj, id: id })
}
})
}
You’re setting your state inside the map. If you think of the map as a loop that means you’re overriding it each iteration. So you will only ever have the last one displaying.
I think that's because your setState() call is in the wrong place (and only one object at a time):
let data =[];
UpdateProduct.map((item, i) => {
let formdata = null;
// do your formdata stuff but append it as part of an array
data.push(formdata);
});
this.setState({ formdata: data });
Then I think it should get all the products.

How to setState() in loop using spread operator

I'm trying to set new values for several fields in nested object in state using spread operator in loop, but it works only for last field.
I have an array "formFields" with names of fields which values I want to overwrite. I use map() to compare each element in array with field in state and switch it's value to "true". But it change value only for the last field in array - "comment".
constructor() {
super();
this.state = {
fields: {
time: false,
date: false,
quantity: false,
comment: false,
},
}
}
getFormFields() {
const formFields = ["time", "quantity", "comment"];
formFields.map(item => {
this.setState({
...this.state.fields,
[item]: true
})
});
}
What should I do to overwrite values for all the fields I want?
Since you are changing state in a loop, and each state you set contains the original item with only changed, the latest change overrides the previous one. Instead, create a new state object with the change, and then setState the object once:
getFormFields() {
const formFields = ["time", "quantity", "comment"];
this.setState(formFields.reduce((r, item) => ({
...r,
[item]: true
}), {}));
}
btw - If the fields you want to set to true are always the same, you can create the object manually, and set it:
getFormFields() {
this.setState({
time: true,
quantity: true,
comment: true,
});
}

Resources