set attributes inside an object's object using prevState syntax - reactjs

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
}
}));

Related

Setting objects within the useState defined object

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

How to avoid firing multiple redux actions with real time firestore listeners

Introduction
A Little Warning: I do use Redux Toolkit
I have bunch of lists, one of which should be active. And depending on some context, active list should be different. For example I have 3 lists (A, B, C) and let's look at following patterns:
List B is active and I decided to create a new list. After creating list D, list D should be active:
List D - active
List C
List B
List A
List B is active and I decided to change the page. When I come back, List B should be active as it was before changing the page.
The problem
As I initiate the setListsAction from the beginning, it always listens to the firestore and gets invoked every time I manipulate with the store (add, remove, update) and then pass all the data to the reducer. For this reason, I can't control which action was actually performed. For this case in my setListsReducer I check if there's already an active list, if so, I don't change it (covering my second pattern in the examples section). However, with such logic I can't set newly created list as active, because there'll be always an active list that's why in my createListAction I pas a newly created list to the payload and in createListReducer I set payload as the active list. However, the caveat of this approach is that both setListsAction and createListAction gets triggered, so redux state gets updated two times in a row, making my components rerender unnecessary. The cycle looks like that:
in createListAction I add list to the firestore
firestore was updated, so setListsAction gets triggered
createListAction dispatches fulfilled action.
My Code
Actions
setListsAction
export const subscribeListsAction = () => {
return async (dispatch) => {
dispatch(fetchLoadingActions.pending());
const collection = await db.collection('lists');
const unsubscribe = collection
.onSnapshot((querySnapshot) => {
const lists = querySnapshot.docs.map((doc) => {
const list = { ...doc.data(), id: doc.id };
return list;
});
dispatch(
fetchLoadingActions.fulfilled({
lists,
})
);
});
};
};
createListAction
export const createListActionAsync = (list) => {
return async (dispatch: Dispatch<PayloadAction<any>>) => {
dispatch(listsLoadingActions.pending());
const docList = await db.collection('lists').add(list);
const fbList = { ...list, id: docList.id };
dispatch(listsLoadingActions.fulfilled(fbList));
};
};
Reducers
setListsReducer
builder.addCase(fetchLoadingActions.fulfilled, (state, { payload }) => {
state.lists = payload.lists;
const activeList = state.activeList
? payload.lists.find((l) => l.id === state.activeList.id)
: payload.lists[0];
state.activeList = activeList;
});
createListReducer
builder.addCase(listsLoadingActions.fulfilled, (state, { payload }) => {
state.activeList = payload;
});
Sum
So I would like you to propose a better way to handle my problem. I tried to solve it, using change type on docChanges but when I init setListsAction, all docs' changes are type of added and workarounds may damage further implementations of the app. Probably, I need to give up real time database and use get method instead.
If you eliminate the createListReducer and listLoadingActions, you should be able to do everything from inside the ListsAction hook. Using await db.collection('lists').add(list) should refire the listener on the lists collection once it's been added to the database successfully.
export const subscribeListsAction = () => {
return async (dispatch) => {
dispatch(fetchLoadingActions.pending());
const collection = db.collection('lists'); // no need to await?
let firstLoad = true; // used to determine whether to use docs or docsChanges
const unsubscribe = collection
.onSnapshot((querySnapshot) => {
if (firstLoad) {
const lists = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
firstLoad = false;
// Get and set initial active list?
dispatch(
fetchLoadingActions.fulfilled({
lists,
})
);
} else {
// optionally fire dispatch(fetchLoadingActions.pending()) again?
const listsCopy = [...state.lists]; // copy the existing list to mutate it
let activeList = state.activeList; // store the current activeList
querySnapshot.docChanges().map((change) => {
if (change.type === "added") {
const thisList = { ...change.doc.data(), id: change.doc.id };
listsCopy.splice(change.newIndex, 0, thisList);
activeList = thisList;
} else if (change.type === "modified") {
listsCopy.splice(change.oldIndex, 1);
listsCopy.splice(change.newIndex, 0, { ...change.doc.data(), id: change.doc.id });
} else if (change.type === "removed") {
listsCopy.splice(change.oldIndex, 1);
if (activeList.id === change.doc.id) {
// the current active list was removed!
activeList = undefined;
}
}
});
dispatch(
fetchLoadingActions.fulfilled({
lists: listsCopy,
activeList: activeList || listsCopy[0] // use activeList or fallback to first list in listsCopy, could still be undefined if listsCopy is empty!
})
);
}
});
return unsubscribe;
};
};
Regarding the active list history, you could either use the URL ?list=some-id to store the selected list with the History API or you could store an array called activeListHistory in your state variable where you push() and pop() to it as necessary (make sure to handle cases where the old list no longer exists and where there are no entries in the array).

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])

change of one value in the state react

In the state I have data array that look like this
{
"offers" : [
{
"model": "shoes",
"availability": false,
"id": 1
},
{
"model": "t-shirt",
"availability": true,
"id": 2
},
{
"make": "belt",
"availability": false,
"id": 3
}
]
}
My task is to change the accessibility field. I have a button - change the availability. After clicking it I want to make the availability of a single field changed
changeAvailability = (ailability, id) => {
console.log(ailability)
console.log(id)
const { data } = this.state;
let newData = [...data]
this.setState({
data: newData.map(x => x.id === id ? x.ailability = !ailability : ailability )
})
}
I have created a function that does not work.
My idea was: I click on the element, pass to the element's id and accessibility. In this function, I create a copy of the state. In the right place, I change the availability and I am sending everything to the state.
This function does not work, nothing happens, I have no idea how to fix it
Seems like the map function is returning nothing ...
Could be something like this
changeAvailability = (ailability, id) => {
console.log(ailability)
console.log(id)
const { data } = this.state;
let newData = [...data]
const updateItem = (x) => {
if (x.id === id) {
x.ailability = !ailability;
}
return x;
};
this.setState({
data: newData.map(updateItem)
})
}
I am assuming you want to click on an element which passes an id and availability of an offer, find a matching entry (on the passed id) in the offers array in the state and change the availability of the found object to what you passed in the function.
I recommend using the callback that this.setState() allows you to use, providing a reference to a copy of the previous state, before the setState was called.
changeAvailability = (passedAvail, passedID) => {
this.setState((prevState) => {
// Make a copy of the offers array in the state
const offersArray = prevState.offers
// Find index of matching obj by id
const matchObjIndex = offersArray.findIndex(
(offerObj) => (
offerObj.id === passedID
)
)
// If a match was indeed found
if (matchObjIndex > -1) {
// Find matching obj from index obtained
const matchObj = offersArray[matchObjIndex]
// Change the availibility of the matched obj and obtain a modified obj
const modmatchObj = Object.assign(matchObj, {
availability: passedAvail
})
// Replace the offer obj by index in the offers array.
offersArray.splice(matchObjIndex, 1, modmatchObj);
}
// Finally set state with the updated offers lists
// If no match was found, it will return the old offers array
return {offers: offersArray }
})
}
You can try something like this to make it work:
changeAvailability = (availability, id) =>
this.setState(prevState => {
return {
offers: prevState.offers.map((e, i) =>
e.id !== id ? e : { ...prevState.offers[i], availability }
)
};
});
The issue with your solution is that you shouldn't modify the items you're iterating while doing a map, but you should just return whatever new data you wanna add.

Resources