Setting objects within the useState defined object - reactjs

Creating a component for users to edit their listings
Scenario:
I have a defined listing object using useState and it gets set using useEffect after the listing is retrieved from the API, shown below:
const listing = useSelector((state) => state.listings.listing);
const [editListing, setEditListing] = React.useState({});
React.useEffect(() => {
getListing(listingId)(dispatch);
setEditListing(listing);
}, []);
Ex Listing Obj
Listing = {
title: 'title'
description: 'something here'
location: { //having trouble setting these properly
country:
city:
state:
},
etc..
}
I can set everything just fine except the listing.location. Using the form all inputs get filled with correct value, but when I change say just City input it sets listing.location.city and everything else goes blank. Here is how I am setting the listing.location object:
const handleChange = (e) => {
const { name, value } = e.target;
setEditListing((listing) => ({
...listing,
location: { [name]: [value] },
}));
};
This should be the only code you need, I am pretty sure I am just not setting the location correct within the listing.

You are replacing the location object with a new object each time. Try spreading the location object:
const handleChange = (e) => {
const { name, value } = e.target;
setEditListing((listing) => ({
...listing,
location: {
...listing.location,
[name]: value
}
}));
};
Note that I have removed the square brackets from value as I am assuming you don't want this to be an array

Related

Handle multiple input field (including file field) using one event handler in react js

I want to handle my all input field including file field using one event handler. Is that possible in react js? I know how to handle multiple text field using one event handler but cant work with file field. My previous code was like that.
const changeHandler = (e) => {
const name = e.target.name;
const value = e.target.value;
setInputs({
...inputs,
[name] : value,
});
}
const {name, email} = inputs;
const formData = new FormData()
formData.append('name', name)
formData.append('email', email)
You could deconstruct the files and do a check whether or not the file input was used.
const changeHandler = ({ target }) => {
const { name, value, files } = target;
if (files) {
setInputs({
...inputs,
[name]: files[0],
});
return;
}
setInputs({
...inputs,
[name]: value,
});
};
CodeSandbox : https://codesandbox.io/s/gallant-cherry-4vufyr?file=/src/App.js
Note that this example only saves a single file.
You could also do a conditional in place of an if statement which is saying: If there is a file, set the file else set the value.
const anotherHandler = ({ target }) => {
const { name, value, files } = target;
setInputs({
...inputs,
[name]: files ? files[0] : value
});
};

setting up custom error validation in react

I am trying to create some custom error validation in React
I have a values obj in state and an error obj in state that share the same keys
const [values, setValues] = useState({
name: "",
age: "",
city: ""
});
const [err, setErr] = useState({
name: "",
age: "",
city: ""
});
i have a very simple handle change and an onSubmit which i want to run my custom validator function inside
const handleChange = (e) => {
setValues({
...values,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault();
validateForms();
};
in my validateForms function my theory is since both my pieces of state share the same keys I am trying to see if any of those values === '' if yes match is the same key in the err obj and set that respective value to the error and then do other stuff in JSX
const validateForms = () => {
for (const value in values) {
if (values[value] === "") {
setErr({
...err,
value: `${value} is a required field`
});
}
}
};
I definitely feel like I'm not using setErr properly here. Any help would be lovely.
link to sandbox: https://codesandbox.io/s/trusting-bartik-6cbdb?file=/src/App.js:467-680
You have two issues. First, your error object key needs to be [value] rather than the string value. Second, you're going to want to use a callback function in your state setter so that you're not spreading an old version of the error object:
const validateForms = () => {
for (const value in values) {
if (values[value] === "") {
setErr(err => ({
...err,
[value]: `${value} is a required field`
}));
}
}
};
A more intuitive way to set errors might be to accumulate them all and just set the error state once:
const validateForms = () => {
const errors = {};
for (const value in values) {
errors[value] = values[value] === "" ? `${value} is a required field` : "";
}
setErr(errors);
};

set attributes inside an object's object using prevState syntax

Each product can be more than one categories so I am trying to create an object of categories inside products object which looks like this:
{
"name":"my product",
"categories":{}
}
All the categories are displayed in checkboxes. This is how my checkbox change event looks like:
const handleChangeCategories = (e, data) => {
const { name, checked } = data;
setProductData(prevState => ({
...prevState.categories,
[name]: checked
}));
};
when handleChangeCategories is fired, It overwrites entire prevState object instead of adding an object in categories.
Expected output:
{
"name":"my product",
"categories:{"cat1":true, "cat2":true}
}
I can achieve it with below code but it does feel react way of doing it:
const handleChangeCategories = (e, data) => {
const { name, checked } = data;
let categories = productData.categories
categories[name] = checked
setProductData(productData)
};
How do I achieve this with ...prevState.categories syntax and how do I handle the scenario when if categories is not defined so it should create categories object?
If I understand your question, you simply want to update the nested "categories" state via checkboxes. Assuming your initial state is
{
name: "my product",
categories: {},
}
Then the following is how you would copy the existing state and nested state. You need to shallowly copy each level of state that is being updated.
const handleChangeCategories = (e, data) => {
const { name, checked } = data;
setProductData(prevState => ({
...prevState, // <-- copy root state object
categories: {
...prevState.categories, // <-- copy nested categories
[name]: checked, // <-- update the specific category
},
}));
};
Spread the previous .categories object into the new categories property, rather than into the whole object returned to setProductData:
setProductData(prevState => ({
...prevState, // or: `name: prevState.name`
categories: {
...prevState.categories,
[name]: checked
}
}));

How to create multiple object in Reactjs?

I am having a onChange function i was trying to update the array optionUpdates which is inside of sentdata by index wise as i had passed the index to the onChange function.
Suppose i update any two values of the input field from option which is inside of postdata therefore the input name i.e. orderStatus with changed value and with order should be saved inside of optionUpdates
For example: Suppose i update the option 1 and option 3 of my postdata further inside of options of orderStatus values so my optionUpdates which is inside of sentdata should look like this
optionUpdates: [
{
Order: 1,
orderStatus: "NEW1"
},
{
Order: 3,
orderStatus: "New2"
}
]
here is what i tried
setSentData(oldValue => {
const curoptions = oldValue.sentdata.optionUpdates[idx];
console.log(curoptions);
curoptions.event.target.name = event.target.value;
return {
...oldValue,
sentdata: {
...oldValue.sentdata.optionUpdates,
curoptions
}
};
});
};
Demo
complete code:
import React from "react";
import "./styles.css";
export default function App() {
const x = {
LEVEL: {
Type: "LINEN",
options: [
{
Order: 1,
orderStatus: "INFO",
orderValue: "5"
},
{
Order: 2,
orderStatus: "INPROGRESS",
orderValue: "5"
},
{
Order: 3,
orderStatus: "ACTIVE",
orderValue: "9"
}
],
details: "2020 N/w UA",
OrderType: "Axes"
},
State: "Inprogress"
};
const [postdata, setPostData] = React.useState(x);
const posting = {
optionUpdates: []
};
const [sentdata, setSentData] = React.useState(posting);
const handleOptionInputChange = (event, idx) => {
const target = event.target;
setPostData(prev => ({
...prev,
LEVEL: {
...prev.LEVEL,
options: prev.LEVEL.options.map((item, id) => {
if (id === idx) {
return { ...item, [target.name]: target.value };
}
return item;
})
}
}));
setSentData(oldValue => {
const curoptions = oldValue.sentdata.optionUpdates[idx];
console.log(curoptions);
curoptions.event.target.name = event.target.value;
return {
...oldValue,
sentdata: {
...oldValue.sentdata.optionUpdates,
curoptions
}
};
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
key={idx}
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e, idx)}
/>
);
})}
</div>
);
}
If I've understood correctly then what you're looking to do is save a copy of the relevant options object in sentdata every time one changes. I think the best way to approach this is by doing all your state modification outside of setPostData, which then makes the results immediately available to both setPostData and setSentData. It will also make the setters easier to read, which is good because you have some quite deeply nested and complicated state here.
A few other things worth noting first:
Trying to use synchronous event results directly inside the asynchronous setter functions will throw warnings. If you do need to use them inside setters, then it is best to destructure them from the event object first. This implementation uses destructuring although it didn't end up being necessary in the end.
You seem to have got a bit muddled up with setSentData. The oldValue parameter returns the whole state, as prev in setPostData does. For oldValue.sentdata you just wanted oldValue. You also wanted curoptions[event.target.name], not curoptions.event.target.name.
So, on to your code. I would suggest that you change the way that your input is rendered so that you are using a stable value rather than just the index. This makes it possible to reference the object no matter which array it is in. I have rewritten it using the Order property - if this value is not stable then you should assign it one. Ideally you would use a long uuid.
{postdata.LEVEL.options.map(item => {
return (
<input
key={item.Order}
type="text"
name="orderStatus"
value={item.orderStatus}
onChange={e => handleOptionInputChange(e, item.Order)}
/>
);
})}
The handleOptionInputChange function will now use this Order property to find the correct objects in both postdata and sentdata and update them, or if it does not exist in sentdata then push it there. You would do this by cloning, modifying, and returning the relevant array each time, as I explained before. Here is the function again with comments:
const handleOptionInputChange = (event, orderNum) => {
const { name, value } = event.target;
/* Clone the options array and all objects
inside so we can mutate them without
modifying the state object */
const optionsClone = postdata.LEVEL.options
.slice()
.map(obj => Object.assign({}, obj));
/* Find index of the changed object */
const optionIdx = optionsClone.findIndex(obj => obj.Order === orderNum);
/* If the orderNum points to an existing object...*/
if (optionIdx >= 0) {
/* Change the value of object in clone */
optionsClone[optionIdx][name] = value;
/* Set postdata with the modified optionsClone */
setPostData(prev => ({
...prev,
LEVEL: {
...prev.LEVEL,
options: optionsClone
}
}));
/* Clone the optionUpates array and all
contained objects from sentdata */
const updatesClone = sentdata.optionUpdates
.slice()
.map(obj => Object.assign({}, obj));
/* Find the index of the changed object */
const updateIdx = updatesClone.findIndex(obj => obj.Order === orderNum);
/* If the changed object has already been
changed before, alter it again, otherwise push
a new object onto the stack*/
if (updateIdx >= 0) {
updatesClone[updateIdx][name] = value;
} else {
updatesClone.push({ Order: orderNum, [name]: value });
}
/* Set sentdata with modified updatesClone */
setSentData(prev => ({
...prev,
optionUpdates: updatesClone
}));
}
};

Setting the state several times inside useEffect sets the last one only

I got a component which has a form to add a new item and its supposed to also update an existing item. I'm trying to set the value of the form fields such that if a user chooses to edit an item, he will have all of the data of the existing item already in the form, which he just needs to edit.
I'm using useEffect for that:
useEffect(() => {
if(props.editedItem)
{
inputChangedHandler(props.editedItem.companyName, "company");
inputChangedHandler(props.editedItem.name, "name");
inputChangedHandler(props.editedItem.description, "description");
}
}, [props.editedItem])
the method inputChangedHandler is setting the form value of a specific field (company, name, description):
const inputChangedHandler = (newVal, inputIdentifier) =>
{
const updatedOrderForm = {
...formSettings
};
const updatedFormElement = {
...updatedOrderForm[inputIdentifier]
};
updatedFormElement.value = newVal;
updatedOrderForm[inputIdentifier] = updatedFormElement;
setFormSettings(updatedOrderForm);
}
The problem here is that only the last field is changed (description in the case of the code). If I changed the lines order and the "name" will be the last, the name info will appear and not the description.
How can I fix it?
You may be overriding your form with staled values (due to closures).
// closure on `updatedOrderForm` value, staled state
const updatedFormElement = {
...updatedOrderForm[inputIdentifier]
};
Try using functional update which provides the most updated state.
const inputChangedHandler = (newVal, inputIdentifier) => {
setFormSettings((prev) => {
const updatedOrderForm = {
...formSettings,
};
const updatedFormElement = {
...updatedOrderForm[inputIdentifier],
value: newVal,
};
return { ...prev, [inputIdentifier]: updatedFormElement };
});
};
Not sure how is your inputChangedHandler method definition is but you can take the value and use the respective hook to set it.
function inputChangedHandler({companyName,name, description}){
setName(name);
setCompanyName(companyName);
...
}
useEffect(() => {
if(props.editedItem)
{
inputChangedHandler(...props.editedItem);
}
}, [props.editedItem])

Resources