I'm fairly new to Redux.
My Web application is an eCommerce website where users have multiple carts (each with an id and name) and can place different items in each cart (inside an items array).
When a user deletes an item from the cart, my console.log() statements show that the carts array is being updated in the store, however the User interface doesn't reflect that unless i insert a new cart object inside of the carts array.
Why can't i update a nested array the same way i update a normal array in the store?
How do i fix this?
My initial Store
const intialStore = {
carts: [],
first_name : "ford",
last_name : "doly"
}
My Reducer Function
export default function reducer (store = intialStore, action) {
let {type, payload} = action;
switch(type) {
case DELETE_ITEM_IN_A_CART : {
let carts = [...store.carts]
let newCarts = carts.map((cartItem, index) => {
if (index == payload.cartIndex){
let array = [...cartItem.items]
array.splice(payload.itemIndex, 1)
cartItem.items = [...array ]
}
return cartItem ;
})
console.log(carts)
//carts[payload.cartIndex].items.splice(payload.itemIndex, 1)
return {...store, carts : newCarts}
}
default:
return {...store}
}
My Action Creator
export const deleteitemInCart = (cartIndex, itemIndex) => {
return {
type: DELETE_ITEM_IN_A_CART,
payload: {
cartIndex,
itemIndex
}
}
}
When you use the spread operator, you're only making a shallow copy. In your reducer, try using JSON.parse(JSON.stringify(carts)) in order to make a deep copy before performing the splice operation. This would ensure you are not mutating any existing state, but operating on a copy and returning a new copy at the end of the reducer.
map returns a new array, it does not mutate the original (which is good, we don't want to mutate it). But this means in order for your mapping actions to be persisted, you need to assign the result to a variable.
let newCarts = carts.map(...)
...
return {...store, carts: newCarts}
Right now, you're just returning the same carts array as the new state.
Related
I am using Redux in my React application. However items are always stored by index, like that =>
I want to store them by ids, like instead of 0 first item's index should be 41. How can I do that?
reducer.js
export function ratedPosts(state=[], action) {
enableES5()
return (
produce(state, draft => {
const rate = action.Rate
switch (action.type) {
case RATE_POST:
draft.unshift({postId: action.postId, rate: rate})
break
case RATE_POST_UPDATE:
draft.map(post => post.postId === action.postId).rate = rate
break
default:
return draft
}
})
)
}
You can't do that with arrays, but you can do that with objects. I see also that you are using Array.unshift to add new posts, keep in mind that arrays do not guarantee the sequence of the items, even though it works most of the time.
You'll need to convert your data structure to use objects instead of array, but in the getter function you could convert to an array so it can be more easily used in the frontend.
You can set an object ID programmatically using [ ]
let myObject = {}
const idOne = 'abc'
const idTwo = 'def'
draft[idOne] = "Hello" // draft.abc === "Hello"
draft[idTwo] = "World" // draft.def === "World"
draft === {
abc: "Hello",
def: "World"
}
I have an array of objects that is saved into a userList useState which is composed of:
[{
firstName: "blah"
lastName: "blah2"
}
{
firstName: "test"
lastName: "test2"
}]
I have a useEffect that calls a function and returns a value. I want to store a new key and value to each user in userList.
useEffect(() => {
userList.forEach((user, index) =>
returnNewValueForNewKeyFunction(user, index).then(newValue => {
userList[index]['newKey'] = newValue
//this console.log shows new field and value
console.log(userList)
//this console.log ALSO shows new field and value
console.log(JSON.stringify(contactList[index]))
})
)
}
}, [])
This is fine if I'm operating out of console.log, but unfortunately I need to render the data onto the page.. in my render I have:
return (
<TableBody>
{userList
.map((user, index) => (
<TableRow>
<TableCell>
{user.newKey}
</TableCell>
)
user.newKey is showing as blank and it seems like the user wasn't updated at all. How can I make it so the value is actually updated and can be read from when rendering?
You shouldnt mutate your list, you should use useState to store your list, so something like this :
const [ state, setState] = useState(userList);
Then when you want to update, do something like this :
const listCopy = [...state];
//Logic to update your list here
listCopy[index][otherindex] = Value;
setState(listCopy)
Hope this helps
You are modifying your userList but not calling your set function on which means React won't know to re-render with the updated state.
Instead of mutating the current state, you should create a new array and then call the set function returned by useState with the updated array after making your changes.
It also looks like your returnNewValueForNewKeyFunction is a promise / async which means each of your item changes are happening async. You'll need to make these synchronous / wait for them all before updating your state to make your state change a single update for the UI.
E.g., putting these both together - if you are doing:
const [userList, setUserList] = useState();
You could do:
useEffect(() => {
// Since can't use an async func directly with useEffect -
// define an async func to handle your updates and call it within the useEffect func
const updateUsers = async () => {
// Create a new array for your updated state
const updatedUserList = [];
// Loop over your values inline so your can await results to make them sync
for (let index = 0; index < userList.length; index ++) {
const user = userList[index];
const newVal = await returnNewValueForNewKeyFunction(user, index);
// Create a shallow copy of the original value and add the newValue
updatedUserList[index] = { ...user, newKey: newValue };
// ... Any other logic you need
}
// Call set with the updated value so React knows to re-render
setUserList(updatedUserList);
};
// Trigger your async update
updateUsers();
}, [])
I have an array of 6 objects which have a uid and nothing else. This is so I can repeat over them and have some placeholder content until an object is ready to be added into the array. I set a unique key when a new object is selected. However if I select the same object twice, even though I'm setting a unique key. It seems to update the unique key on the duplicate item (even though the unique key is different).
Might be easier to see the code/app in action here, an example of the problem would be clicking squirtle then blastoise, take a note of the uid's shown. Then click squirtle again and for some reason it updates the old squirtle with the new squirtles uid causing a duplicate key error. https://codesandbox.io/s/l75m9z1xwq or see code below. Math.random is just placeholder until I can get this working correctly.
const initState = {
party: [
{ uid: 0 },
{ uid: 1 },
{ uid: 2 },
{ uid: 3 },
{ uid: 4 },
{ uid: 5 }
]
};
When I click on something this is triggered:
handleClick = pokemon => {
// setup a uid, will need a better method than math.random later
pokemon.uid = Math.random();
this.props.addToParty(pokemon);
};
This then calls a dispatch which triggers the following reducer. Which essentially just checks if the object has no normal ID then replace the content with the payload sent over. It does this but also somehow updates any previous objects with the same uid even though the if statement does not run against them.
const rootReducer = (state = initState, action) => {
if (action.type === "ADD_POKEMON") {
let foundFirstEmptyPoke = false;
const newArray = state.party.map((pokemon, index) => {
if (typeof pokemon.id === "undefined" && foundFirstEmptyPoke === false) {
foundFirstEmptyPoke = true;
pokemon = action.payload; // set the data to the first object that ios empty
}
// if we get to the last pokemon and it's not empty
if (index === 5 && foundFirstEmptyPoke === false) {
pokemon = action.payload; // replace the last pokemon with the new one
}
return pokemon;
});
return {
party: newArray
};
}
return state;
};
The problem here is that, when you click to select a pokemon, you mutate the data you retrieved from the API:
handleClick = pokemon => {
pokemon.uid = Math.random(); // HERE
this.props.addToParty(pokemon);
};
You actually mutate the react state. What you should do is clone your pokemon data object, add an uid to the clone you just generated and update your redux state with it:
handleClick = pokemon => {
this.props.addToParty({
...pokemon,
uid: Math.random()
});
};
That way, no references to the actual react state are kept. Because that was what was happening when you say it updates the old squirtle with the new squirtles uid. When you tried to add another pokemon, you updated the data you retrieved from your API which was also referenced from your first pokemon slot (from your redux state).
In react/redux it's always better to not mutate objects:
this.props.addToParty({...pokemon, uid: Math.random()});
You are mutating the state. Use spread syntax *** to copy the state before updating.
return {
...state,
party: newArray
}
I have the following structure. I have a Company object which contains many Person objects in an array.
companies: CompanyInterface = {
persons: PersonInterface[] = []
}
I'm trying to show the persons that belongs to this company in a table. However, when I add/delete persons to the persons array, it should automatically be updated.
Right now my reducer looks like this:
// Initial state
companies: CompanyInterface[];
// Reducer
case Constants.GLOBAL_COMPANY_ADD_PERSON:
[action.payload.person].concat(state.companies.filter(company => company.id === action.payload.id)[0].persons));
return {
...state
};
Although I can verify concat works properly (I can see [Person] when I consolle log it) it doesn't update anything on the store.
So what is the best way to update an array in Redux, which is a property in the parent object?
Thank you.
You might want to take a look at Array.prototype.concat, as it does not mutate the array in place, but returns a new array. In your example you are not mutating the state.
Instead, you need to replace the old value with the new one. For instance:
case 'ADD_ELEMENT':
const { elements: previousElements } = state;
const newElement = action.payload;
const newElements = previousElements.concat(newElement)
return {
...state,
elements: newElements
};
I have an immutable List that looks like this:
this.state = {
suggestedUsers: fromJS([
{
user: {
client_user_id: "1234567890",
full_name: "marty mcfly",
image: "imageURL",
role_name: "Associate Graphic Designer",
selected: false
}
},
{
user: {
client_user_id: "0987654321",
full_name: "doc",
image: "imageURL",
role_name: "Software Engineer",
selected: false
}
}
)]
This is used in a div that displays this information in the UI.
When I click on the div, I have a function that is fired that looks like this:
selectUser(clientUserId){
// set assessments variable equal to the current team from the state
let assessments = fromJS(this.state.suggestedUsers)
let selectAssessor
// set a variable called selectedUsers equal to the results of filtering over the current suggestedUsers from the state
let selectedUsers = assessments.filter((obj) => {
// store immutable retrieval of the client user id in a variable called userId
let userId = obj.getIn(["user", "client_user_id"])
// when the user clicks 'Add' user, if the id of the user matches the selected user id
// the user, represented here by obj, is pushed into the selectedUsers array stored in the state.
if(userId === clientUserId){
return obj.setIn(["user", "selected"], true)
}
// If the user id is not equal to the selected user, that team member is kept in the
// current team array represented by the state.
return userId !== clientUserId
})
// update the state with the current representation of the state determined by the user
// selected team members for assessment requests
this.setState({
suggestedUsers: selectedUsers
})
}
The core of my question is this:
I would like to update the value of the 'selected' key in the users object to false, when this function is invoked.
I'm aware that I can't mutate the List I'm filtering over directly, but I've tried may different approaches to getting the selected value updated (i.e. using updateIn, and setIn). I know I need to set the result of calling setIn to a variable, and return that to the List I'm filtering over, but I can't get the value to update in the existing List. Any help is greatly appreciated. Thanks!
I've verified that this works the way it should when I change the value manually. How can I change it with immutable by updating this one List.
=========================================================================
Thank you to the community for your feedback. Filtering, and mapping did turn out to be overkill. Using immutability-helper, I am able to update the selected value of a particular user at the index that is clicked. One caveat that was not mentioned is using merge to bring your updated data into your previous data. After updating with immutability helper, I push the updated value into an array, then make it a List, and merge it into my original data. Code below:
let users = this.state.teamAssessments
let selectedArray = []
users.map((obj, index) => {
let objId = obj.getIn(["user", "client_user_id"])
if(objId === clientUserId){
const selectedUser = update(this.state.teamAssessments.toJS(), {
[index]: {
user : {
selected: {
$set: true
}
}
}
})
selectedArray.push(selectedUser)
}
})
let updatedArray = fromJS(selectedArray).get(0)
let mergedData = users.merge(updatedArray)
this.setState({
teamAssessments: mergedData
})
You need immutability-helper. Basically, instead of cloning the entire object you just modify small pieces of the object and re-set the state after you are finished.
import update from 'immutability-helper';
const newData = update(myData, {
x: {y: {z: {$set: 7}}},
a: {b: {$push: [9]}}
});
this.setState({varName: newData});
In other words, I would ditch the fromJS and the modifying of the array while enumerating it. First, enumerate the array and create your updates. Then, apply the updates separately. Also, to me the "selected" var seems redundant as you know if they are selected because the name of the array after filtration is "selectedUsers."
If I understand your question correctly, here's what I would suggest:
selectUser(clientUserId) {
let suggestedUsers = this.state.suggestedUsers.map(
userBlock =>
userBlock.setIn(
['user', 'selected'],
userBlock.getIn(['user', 'client_user_id']) === clientUserId
)
);
this.setState({
suggestedUsers,
});
}
To confirm -- you are just trying to modify state.suggestedUsers to have selected be true for the selected user, and false for everyone else? Sounds perfect for Immutable's map function (rather than filter, which will just return the elements of the list for which your predicate function returns something truthy).
BTW, you have an error in your object passed into fromJS -- you need an extra }, after the first assessor_assessment block.