Adding items into array inside object without mutating state - javascript

For reference this is the initial state:
const initialState = {
dogs: [],
cats: [],
petToPreview: {},
petToAdopt: {},
};
I have the following case that I am trying to solve
case 'ADD_NEW_DOG':
let pleaseWork = {
...state,
dogs: [action.dog],
};
console.log(pleaseWork);
The action creator I am using is this:
export const addNewDog = (pet) => {
return {
type: 'ADD_NEW_DOG',
dog: pet,
};
};
The question I am trying to solve is that this case adds the new dog to the end of the dogs array (without mutating the previous state). The way that I have it set up right now, is that it adds the action correctly, However, every time a new 'dog' tries to get added, it just overwrites the previous one.
When I log out my case I get this:
{
dogs: [ { id: 1, name: 'Taylor' } ],
cats: [],
petToPreview: {},
petToAdopt: {}
}
However, Like I mentioned any new action that gets added, overwrites this. I have tried pushing into the 'clone' however this I know mutates the array, so I do not know where to go from here.

You need to clone dogs array and add new entry at the end.
case 'ADD_NEW_DOG':
let pleaseWork = {
...state,
dogs: [...state.dogs, action.dog],
};
console.log(pleaseWork);

You replace the new array with the older one,
case 'ADD_NEW_DOG':
let pleaseWork = {
...state,
dogs: [...state.dogs,action.dog],
};

Related

What do you suggest to make undo?

I use a series of data as follows:
[
{
name: 'name1',
background:'red',
child:[
{
name:'',
id:'',
color:'',
text:'',
border:''
},
{
name:'',
id:'',
color:'',
text:'',
border:''
}
]
},
{
name: 'name2',
background:'red',
child:[
{
name:'',
id:'',
color:'',
text:'',
border:''
},
{
name:'',
id:'',
color:'',
text:'',
border:''
}
]
}
]
I'm going to save all the changes to another variable, and I used a deep copy to do that, but when I log in, the variables are the same.I need to children all the children changes too.
I wrote it in Reducers
const Reducers =(state = initialState, action) => {
switch (action.type) {
case NEW_OBJECTS_PAST:
const OldPast = JSON.parse(JSON.stringify(state.past))
const newDate = JSON.parse(JSON.stringify(state.present))
// const newDate = _.cloneDeep(state.present);
const newPast = [
OldPast,
newDate
];
return {
...state,
past : _.cloneDeep(newPast) ,
}
case OBJECTS:
return {
...state,
present: action.objects,
// future:[]
}
Do you suggest another way to build undo in REACT and REDUX ?
I tried the libraries available for this, but got no answer.
First two remarks :
you should never deep clone parts of your state, it doesn't bring you any benefits as the state is immutable anyway, but is very detrimental for memory usage and for performance (both when doing the deep cloning and then when using the deep cloned state),
you should use Redux toolkit, which makes it way easier to write immutable reducers and prevent many errors.
To implement undo, I'm not sure what your actions are supposed to mean but you can do it as follows
the state contains state.present (the current state) and state.past (an array of past states)
whenever you do an action that you want to undo, you push the current state.present at the end of state.past and compute the new state.present
whenever you want to undo, you pop the last element of state.past and put it in state.present.
In your code I can't see any undo action, and you're also building nested arrays because of new Past = [oldPast, new Date], you most likely meant to spread oldPast.

Redux not updating state correctly -- state stays as empty array despite adding an object in the line prior

I'm trying to update my React Redux state using the usual practice. You immutably copy your current state into the return function and then overwrite the properties which are to have a new value.
My expected result is to have
state.copy
update to
[
{ webCopy: "", id: 0 },
{ webCopy: "", id: 1 }
]
Unfortunately, it's an empty array despite returning the copy value as a new array with a JS object inside.
I've tried looking at StackOverflow search results for "react redux state not updating correctly" but none of the search results seem to match my situation.
In my app I've even checked: Only
case actionTypes.ADD_COMPONENT
(the first one in the reducer) activates at this part of the app. It is literally a button click which triggers
ADD_COMPONENT
on its own. There is no activity from any other reducer that could be overwriting my state with an empty [] array.
Why am I ending up with an empty array at the end of case actionTypes.ADD_COMPONENT?
My console.log() statement even shows a JavaScript Object being the value for newCopy RIGHT BEFORE I RETURN a new state.
So here is reducer.js. I've uploaded the COMPLETE reducer instead of just case actionTypes.ADD_COMPONENT where the error is taking place:
import * as actionTypes from "./constants";
let component = "";
let idIteration = null;
let stateArray = [];
let tempCopy = [];
const initialState = {
components: [],
uniqueIdCounter: 0,
currentPage: 1,
copy: [],
siteURL: "/salespage/"
};
const reducer = (state = initialState, action) => {
switch (action.type) {
// you can probably ignore everything outside of this switch statement
// this is the important switch statement
case actionTypes.ADD_COMPONENT:
stateArray = [...state.components];
console.log("state:", state);
console.log("State Copy:", state.copy);
component = action.payload[0]; // will be "Header", "Headline", "Text Area", "Image", "Email Field", "Footer"
if (component === "Header") {
// append "Header" component to the beginning of the list
stateArray.unshift({
type: component,
id: state.uniqueIdCounter
});
} else {
// push component to the end of the list
stateArray.push({
type: component,
id: state.uniqueIdCounter
});
}
idIteration = state.uniqueIdCounter + 1;
// SEND HELP. How could state.copy possibly be anything other than a JS object after these lines?
let newCopy = [...state.copy];
newCopy.push({ webCopy: "", id: action.payload[1] });
console.log("TTTTTTTTT", newCopy);
return {
...state,
components: stateArray,
uniqueIdCounter: idIteration,
copy: newCopy // why doesn't this update the array to contain newCopy? it should.
};
// i don't know how any of this could possibly cause my state.copy to be equal to []
case actionTypes.SET_NEW:
console.log("Activating SET_NEW");
// uploads the newly reordered set of components to state
let uploadNewOrder = [...action.payload];
return {
...state,
components: uploadNewOrder
};
case actionTypes.DEL:
console.log("activating DEL");
// uploads the state less the deleted item
let uploadShortened = [...action.payload];
return {
...state,
components: uploadShortened
};
case actionTypes.PAGE_CHANGE:
console.log("activating PAGE_CHANGE");
stateArray = [...state.components];
return {
...state,
// action.payload is set in each page's ComponentDidMount()
currentPage: action.payload
};
case actionTypes.NEW_VAR:
console.log("activating NEW_VAR");
// case "NEW_VAR" fires from Customize's renderStateComponents()
stateArray = [...state.components];
tempCopy = Object.assign([], state.copy); // avoids the TypeError bug with [...state.copy]
// push an empty copy datapoint to state with a unique id to use in identifying which copy goes where in interface
let newInputFieldNumber = { webCopy: "", id: action.payload };
tempCopy.push(newInputFieldNumber);
return {
...state,
components: stateArray,
copy: tempCopy
};
case actionTypes.ADD_COPY:
console.log("activating ADD_COPY");
tempCopy = [...state.copy]; // immutably copy state.copy
let textToAdd = action.payload[0];
let indexToFind = action.payload[1];
for (let i = 0; i < tempCopy.length; i++) {
if (tempCopy[i].id === indexToFind) {
// Modify the JS object linked to the appropriate input field
tempCopy[i] = { webCopy: textToAdd, id: indexToFind };
}
}
return {
...state,
components: stateArray,
copy: tempCopy
};
case actionTypes.SET_URL:
console.log("activating SET_URL");
stateArray = [...state.components];
// TODO: handle cases like user entered www.etc.com and https://www.test.com
let domain = action.payload;
const notAllowed = [
"https://www.",
"www.",
".",
"www",
"com",
"net",
"org",
".com",
".net",
".org"
];
for (let i = 0; i < notAllowed.length; i++) {
if (domain.includes(notAllowed[i])) {
domain = domain.replace(notAllowed[i], "");
}
}
return {
...state,
components: stateArray,
siteURL: "/salespage/" + domain
};
default:
return state;
}
};
export default reducer;
Any suggestions what to try? I tried adding this case to my reducer and activating it in the line after .ADD_COMPONENT and it's still yielding an empty array:
case actionTypes.PREP_COPY:
let prepCopy = [...state.copy];
prepCopy.push({ webCopy: "", id: action.payload });
return {
...state,
copy: prepCopy
};
I gave the offending variable, newCopy, a unique name as to not use global scope. In case that matters somehow.
What other code could I show? Only a reducer can affect Redux state and there is no other code running besides .ADD_COMPONENT and (now) .PREP_COPY
EDIT:
As per suggestions, I've tried using the spread operator with my variables while returning state. The code now works if I use the Spread operator in BOTH Reducer Actions only. Using it in just one of them still yields an empty array. Like so:
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.ADD_COMPONENT:
// edited out some code...
let newCopy = [...state.copy];
newCopy.push({ webCopy: "", id: action.payload[1] });
console.log("newCopy", newCopy);
return {
...state,
components: stateArray,
uniqueIdCounter: idIteration,
copy: [...newCopy]
};
case actionTypes.PREP_COPY:
console.log("State.copy:", state.copy);
let prepCopy = [...state.copy];
prepCopy.push({ webCopy: "", id: action.payload });
console.log("PREPCOPY:", prepCopy);
return {
...state,
copy: [...prepCopy]
};
So either I use both those Actions at once, or nada. Literally: When I use both, I get two JS Objects added per cycle. When I use only one, I get 0 JS objects added per cycle. Wut.
Like, should I report a bug to React's team?
2nd edit: here is a fully working code sandbox. Check the console.log statements when you click a button https://codesandbox.io/s/lucid-heisenberg-iczww
Have you tried returning copy as a new array? It's solved problems for me before
You can just use the spread operator to create it:
return {
...state,
components: stateArray,
uniqueIdCounter: idIteration,
copy: [...newCopy]
};
Another, similar method would eliminate the need to push the value onto a temporary variable:
return {
...state,
components: stateArray,
uniqueIdCounter: idIteration,
copy: [...state.copy, { webCopy: "", id: action.payload[1] }]
};
The problem is in Pallette.js in your codesandbox link. On line 29, you call this.props.copy.pop(), which removes the last element from copy. props.copy is the same array as state.copy, so you're mutating state every time addComponent is called. If you change this:
nextCopyId = this.props.copy.pop().id + 1;
to this:
nextCopyId = this.props.copy[this.props.copy.length - 1].id + 1;
or this:
nextCopyId = [...this.props.copy].pop().id + 1;
state.copy is no longer an empty array.
In general when working with React and Redux, make sure to avoid mutative methods as much as possible. Prefer concat over push or unshift. Prefer map, filter, or reduce (which all return a copy of the array) over forEach or a for loop (which encourages you to mutate the array). Sometimes mutation is unavoidable, but be cautious and aware that when you mutate arrays/objects coming from props, you're affecting the state as well!

How to update only one specific property of nested object in associative array

I have associative array.
It's a key(number) and value(object).
I need to keep state of this array same as it is I just need to update one object property.
Example of array:
5678: {OrderId: 1, Title: "Example 1", Users: [{UserId: 1}, {UserId: 2}, {UserId: 3}]}
5679: {OrderId: 2, Title: "Example 2", Users: [{UserId: 1}, {UserId: 2}, {UserId: 3}]}
I need to update Users array property.
I tried this but it doesn't work:
ordersAssociativeArray: {
...state.ordersAssociativeArray,
[action.key]: {
...state.ordersAssociativeArray[action.key],
Users: action.updatedUsers
}
}
This is data inside reducer.
What I did wrong how to fix this?
Something that might help.
When I inspect values in chrome I check previous value and value after execution of my code above:
Before:
ordersAssociativeArray:Array(22) > 5678: Order {OrderId: ...}
After:
ordersAssociativeArray: > 5678: {OrderId: ...}
Solution (code in my reducer)
let temp = Object.assign([], state.ordersAssociativeArray);
temp[action.key].Users = action.updatedUsers;
return {
...state,
ordersAssociativeArray: temp
}
So this code is working fine.
But I still don't understand why? So I have solution but would like if someone can explain me why this way is working and first not?
If it could help here how I put objects in this associative array initialy:
ordersAssociativeArray[someID] = someObject // this object is created by 'new Order(par1, par2 etc.)'
What you are doing is correct, as demonstrated by this fiddle. There may be problem somewhere else in your code.
Something that I would recommend for you is to separate your reducer into two functions, ordersReducer and orderReducer. This way you will avoid the excessive use of dots, which may be what caused you to doubt the correctness of your code.
For example, something like:
const ordersReducer = (state, action) => {
const order = state[action.key]
return {
...state,
[action.key]: orderReducer(order, action)
}
}
const orderReducer = (state, action) => {
return {
...state,
Users: action.updatedUsers
}
}
I hope you find your bug!
Update
In your solution you use let temp = Object.assign([], state.ordersAssociativeArray);. This is fine, but I thought you should know that it is sometimes preferable to use a {} even when you are indexing by numbers.
Arrays in javascript aren't great for representing normalized data, because if an id is missing the js array will still have an undefined entry at that index. For example,
const orders = []
array[5000] = 1 // now my array has 4999 undefined entries
If you use an object with integer keys, on the other hand, you get nice tightly packed entries.
const orders = {}
orders[5000] = 1 // { 5000: 1 } no undefined entries
Here is an article about normalizing state shape in redux. Notice how they migrate from using an array in the original example, to an object with keys like users1.
The problem can be that you're using array in the state but in the reducer you're putting as object. Try doing:
ordersAssociativeArray: [ //an array and not an object
...state.ordersAssociativeArray,
[action.key]: {
...state.ordersAssociativeArray[action.key],
Users: action.updatedUsers
}
]
It will put ordersAssociative array in your state and not an object.

Updating an array of objects in redux

The initial state looks like this:
const INITIAL_STATE = {
myArray: []
};
Now in my reducer, I want to append a new object to the existing array.
I came up with something like this but it doesn't work as expected.
case ADD_TO_ARRAY:
return {
...state,
myArray: [...state[ { action.payload.key: action.payload.value} ]]
};
Note: I want to create a new object, in line, using the key and value passed in the action payload.
with ES6 you can have dynamically calculated object keys
just add a variable to be evaluated in square brackets []
case ADD_TO_ARRAY:
return {
...state,
myArray: [...state.myArray, [ { [action.payload.key]: action.payload.value} ]]
};

Object.assign does not update my state (React)

I have an app which has a few users. I would now like to be able to create a new user. So I have created this actionCreator:
export const createUser = (first, last) => {
console.log("You are about to create user: XX ");
return {
type: 'USER_CREATE',
first: first,
last: last,
payload: null
}
};
I am dealing only with first & last names for now. The actionCreator gets its parameters from the container. There is a button which calls the actionCreator like so:
<button onClick={() =>this.props.createUser(this.state.inputTextFirstName, this.state.inputTextLastName)}>Submit</button>
My UserReducer looks like this:
/*
* The users reducer will always return an array of users no matter what
* You need to return something, so if there are no users then just return an empty array
* */
export default function (state = null, action) {
if(state==null)
{
state = [
{
id: 1,
first: "Bucky",
last: "Roberts",
age: 71,
description: "Bucky is a React developer and YouTuber",
thumbnail: "http://i.imgur.com/7yUvePI.jpg"
},
{
id: 2,
first: "Joby",
last: "Wasilenko",
age: 27,
description: "Joby loves the Packers, cheese, and turtles.",
thumbnail: "http://i.imgur.com/52xRlm8.png"
},
{
id: 3,
first: "Madison",
last: "Williams",
age: 24,
description: "Madi likes her dog but it is really annoying.",
thumbnail: "http://i.imgur.com/4EMtxHB.png"
}
]
}
switch (action.type) {
case 'USER_DELETED':
return state.filter(user => user.id !== action.userIdToDelete);
case 'USER_CREATE':
console.log("Action first:" + action.first);
console.log("Action last:" + action.last);
Object.assign({}, state, {
id: 4,
first: action.first,
last: action.last,
age: 24,
description: "Some new Text",
thumbnail: "http://i.imgur.com/4EMtxHB.png"
});
return state;
}
return state;
}
Now I have a few questions.
1) Is this the proper way to do this, or am I writing bad code somewhere? Keep in mind that I am trying to use Redux here, I am not entirely sure though whether I am not sometimes falling back into React without Redux
2) Am I doing the state thing correctly? I initially used a tutorial and am now building upon that, but I am not sure why state seems to be an array:
state = [ <--- Does this mean that my state is an array?
{
id: 1,
// and so on ...
I am very confused by this, since in other Tutorials state is just an object containing other smaller objects and its all done with parentheses { }
3) What would be the best way to create a new user. My Object.assign does not work, it does not update anything, and I am not sure where the mistake lies.
4) And, relatedly, how could I update one individual user or a property of one individual user?
As Flyer53 states you need to set the state to the return value of Object.assign() as this is designed to not mutate state it will not change the value of the state you're passing in.
The code's fine; I'd tend to use just one property on the action in addition to its type, so have a property of (say) user that is an object containing all the user data (first name, last name etc).
I believe it's quite idiomatic to define a default state outside of the reducer and then set this as the default value for the state parameter in the reducer function:
export default function (state = initialState, action) {
For a brilliant introduction by its creator, see https://egghead.io/courses/getting-started-with-redux
State can be any shape you like. As an application grows in complexity it will usually be represented as an object composed of different sections of data. So, for example, in your case in could be comprised of an array of users and, say, an 'order by' that could apply to some UI state):
{ users: [], orderBy: 'lastName' }
If you carry on using an array of users as the state then you can use the ES6 spread operator to append the new user, for example:
newState = [ ...state, action.user ];
whereas if you move to using an object for state, the following would similarly append a user:
newState = Object.assign({}, state, { users: [ ...state.users, action.user ] };
Finally, to update a single user you could just use map against the array of users as follows (this is obviously hardcoded, but you could match, say, on id and update the appropriate properties).
let modifiedUsers = state.users.map((user) => {
if (user.id === 3) {
user.name = user.name + '*';
}
return user;
});
let newState = Object.assign({}, state, { users: modifiedUsers });
There's maybe an easier way to log the state(users) in an object (not in an array as in your example code above) that works without Object.assign() which is supposed to work with objects, not arrays:
var state = {
user1: {
id: 1,
first: "Bucky",
last: "Roberts",
age: 71,
description: "Bucky is a React developer and YouTuber",
thumbnail: "http://i.imgur.com/7yUvePI.jpg"
}
};
state['user' + 2] = {
id: 2,
first: "Joby",
last: "Wasilenko",
age: 27,
description: "Joby loves the Packers, cheese, and turtles.",
thumbnail: "http://i.imgur.com/52xRlm8.png"
};
console.log(state);
console.log(state.user2);
Just an idea ...

Categories