Related
I am currently working on a trial app where I allow users to vote on the best anecdote and when a user votes, the action creator should return a new sorted array of objects. However, i keep getting this error message:
An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.
I am kind of confused with this error message because I do not see where I have modified the original state because I would assume filter and map would not mess with the state. Where am i wrong? Here is my code:
import _, { sortBy } from 'underscore';
import { createSlice, current } from '#reduxjs/toolkit';
const anecdotesAtStart = [
'If it hurts, do it more often',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.'
]
const getId = () => (100000 * Math.random()).toFixed(0)
const asObject = (anecdote) => {
return {
content: anecdote,
id: getId(),
votes: 0
}
}
const initialState = anecdotesAtStart.map(asObject)
const anecdoteSlice = createSlice({
name: 'anecdote',
initialState,
reducers: {
createNotification(state, action) {
return action.payload;
},
increaseVote(state, action) {
const currentArray = state.filter(obj => obj.id === action.payload)
const newState = state.map(obj => {
// ποΈ if id equals 2 replace object
if (obj.id === currentArray[0].id) {
currentArray[0].votes += 1
return currentArray[0]
}
// ποΈ otherwise return object as is
return obj;
});
const sortedArray = _.sortBy(newState, 'votes')
return sortedArray
},
createAnecdote(state, action) {
const newAnecdote = action.payload
const initializedAnecdote = asObject(newAnecdote)
const updatedAnecdotes = state.concat(initializedAnecdote)
return updatedAnecdotes;
},
}})
export const { createNotification, increaseVote, createAnecdote} = anecdoteSlice.actions
export default anecdoteSlice.reducer
I believe the error is occurring in increaseVote:
increaseVote(state, action) {
const currentArray = state.filter(obj => obj.id === action.payload)
const newState = state.map(obj => {
// ποΈ if id equals 2 replace object
if (obj.id === currentArray[0].id) {
currentArray[0].votes += 1
return currentArray[0]
}
// ποΈ otherwise return object as is
return obj;
});
const sortedArray = _.sortBy(newState, 'votes')
return sortedArray
},
I am confused with this error message because I do not see where I have modified the original state because I would assume the filter and map would not mess with the state. Where am I wrong?
Answer to this question
Since your state is an array of objects when you run this line of code, you think that the filter() is returning a new array, you are right but the items which are of type reference get copied into the new array
So every item inside the currentArray will point to items inside the original array and if you are modifying any item from currentArray, the changes will be reflected in the original array as well. This concept is known as shallow copies
const currentArray = state.filter(obj => obj.id === action.payload)
Shallow Copy ->
A shallow copy of an object is a copy whose properties share the same references (point to the same underlying values) as those of the source object from which the copy was made. As a result, when you change either the source or the copy, you may also cause the other object to change too β and so, you may end up unintentionally causing changes to the source or copy that you don't expect. That behaviour contrasts with the behaviour of a deep copy, in which the source and copy are completely independent.
For example, if in a shallow copy named copy of an array object, the value of the copy[0] element is {"list":["butter","flour"]}, and you do copy[0].list = ["oil","flour"], then the corresponding element in the source object will change, too β because you selectively changed a property of an object shared by both the source object and the shallow copy.
However, if instead you do copy[0] = {"list":["oil","flour"]}, then the corresponding element in the source object will not change β because in that case, you're not just selectively changing a property of an existing array element that the shallow copy shares with the source object; instead you're actually assigning a completely new value to that copy[0] array element, just in the shallow copy.
In JavaScript, all standard built-in object-copy operations (spread syntax, Array.prototype.concat(), Array.prototype.slice(), Array.from(), Object.assign(), and Object.create()) create shallow copies rather than deep copies.
reference -> https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy
State is defined like so:
const [items, setItems] = useState([] as CartItemType[]);
const [id, setId] = useState<number | undefined>();
In this case, id is totally useless. Don't need it in my app at all.
However, if I try to update items, the state variable doesn't change and the UI doesn't reload, unless I also update id:
useEffect(() => console.log("reload")); // only fires if I include setId
const clickItem = (item: CartItemType) => {
let tempItems = data;
// #ts-ignore
tempItems[item.id - 1].animation =
"item animate__animated animate__zoomOut";
setItems(tempItems!); // "!" to get rid of ts complaint about possible undefined value
setId(item.id); // nothing happens if I don't include this
};
// ... inside the return, in a map fn
<Item
item={item}
handleAddToCart={handleAddToCart}
clickItem={clickItem}
/>
// inside Item component
<StyledItemWrapper
className={item.animation}
onClick={() => {
clickItem(item); // item = an obj containing an id and an animation property
}}
>
Why is setId necessary here? What is it doing that setItems isn't?
The reason is because setState uses Object.is equality by default to compare the old and the new values and tempItems === items even after you mutate one of the objects inside of it.
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects.
You can solve this by only mutating a copy of the array:
let tempItems = [...data]; // You call it `data` here, but I assume it's the same as `items` above.
but you'll run into the same problem if anything depends on item changing, so then you have to copy everything, which is more expensive:
let tempItems = data.map(d => ({...d}));
The alternative is to only copy what you're going to mutate (or switch to an immutable datastructure library like Immer or Immutable.js):
let lastIndex = data.length - 1;
// Copy _only_ the value we're going to mutate
let tempItems = data.map((d, i) => i !== lastIndex ? d : {...d});
Each time you call setItems you would have to pass it a new array if you want to render. If you mutate the same array then the equality checks that react does to see if things have changed will always tell that array hasn't changed so no rendering will take place.
Eg:)
let a = [{animation: true}]
let b = a
a[0].animation = false
console.log(a === b) // This returns true
You can instead use map to loop over the array and return a new array.
const clickItem = (item: CartItemType) => {
let tempItems = data.map((a, index) => {
if (index !== (item.id - 1)) { return a }
return {...a, animation: "item animate__animated animate__zoomOut"}
})
setItems(tempItems!);
};
Common mistake. I do this all the time. States are considered changed if the underlying object reference changes.
So these objects don't change:
Given:
interface {
id: number
type_name: string
}
const [items, setItems] = useState([] as CartItemType[]);
let curItems = items;
When:
curItems.push(new CartItemType());
setItems(curItems);
Expected:
state change is triggered
Actual:
state change is not triggered
Now... When:
curItems.push(new CartItemType());
setItems([...curItems]);
Expected:
state change is triggered
Actual:
state change is triggered
Same goes for objects. The fact is if you change underlying properties of an object (and arrays, since arrays are objects with numerical property names) JavaScript does not consider the object is changed because the reference to that object is unchanged. it has nothing to do with TypeScript or React and is a general JS thing; and in fact this is what React uses as a leverage for their Ref objects. If you want them considered different, change the reference by creating a new object/array by destructing it.
I have a method in an angular component that pass component property array in method, but property value is not changing:
private districtModel : LocationModel[] = [];
valueChange(apiService : ApiService,control : FormControl,url,targetModel: LocationModel[]) {
control.valueChanges.subscribe(newValue=>{
if(control.value === ""){
console.log("empty");
console.log(this.stateTemp);
this.stateModel = this.stateTemp;
}
else{
this.stateModel = this.filterValues(newValue,this.stateModel);
}
if(this.stateModel.length===1){
console.log(this.stateModel[0].id);
apiService.GetLocationById<LocationModel[]>(url,this.stateModel[0].id)
.subscribe(data=> {
targetModel = data;
//console.log(this.districtModel);
});
}
});
}
Function calling
this.valueChange(apiService,this.societyForm.controls['State'] as
FormControl,"https://localhost:44355/Location/GetDistrict?StateId=",this.districtModel);
I want to change the value that is this.districtModel inside function but it's not changing
Here
targetModel = data;
you assign data to the local variable targetModel which will not change this.districtModel. What you could do, is clear the original array and assign new values to it:
targetModel.splice(0, targetModel.length, ...data);
This removes targetModel.length items from the array beginning at 0 and adds all items from data.
If your object LocationModel contains nested objects, you can deep copy your array with lodash, before passing it to the function:
import * as _ from "lodash";
// ...
const clonedDistrictModel = _.cloneDeep(districtModel);
If you only need shallow copy, use the spread operator like that:
const clonedDistrictModel = { ...districtModel }
Take a look here if you want to know different types of copy: https://www.freecodecamp.org/news/copying-stuff-in-javascript-how-to-differentiate-between-deep-and-shallow-copies-b6d8c1ef09cd/
You can deep copy object to another object the following code.
JSON.parse(JSON.stringify(Object))
I have an Array with one or more objects and I want to filter out all null properties:
asset = [{"ObjId":177791,"ObjCreditlineM":"DEU","ObjReprorechtM":null,"ObjKommentarM":null,"ObjZustandM":null,"ObjReserve01M":null,"ObjReserve02M":null,"ObjFeld01M":null,"ObjFeld02M":null,"ObjFeld03M":null,"ObjFeld04M":"Foto","ObjFeld05M":null,"ObjFeld06M":null,"ObjFeld07M":null,"ObjFeld01S":null,"ObjFeld02S":null,"ObjFeld03S":null,"ObjFeld04S":null,"ObjFeld05S":null,"ObjFeld06S":null,"ObjFeld07S":null,"ObjFeld01F":0,"ObjFeld02F":0,"ObjFeld01D":null,"ObjFeld02D":null,"ObjInv01S":null,"ObjInv02S":null,"ObjInv03S":null,"ObjInv04S":null,"ObjInv05S":null,"ObjInv06S":null,"ObjDinId":0,"ObjReferenz01Id":null,"ObjReferenz02Id":null,"ObjTransferId":null,"ObjGesperrtS":null,"ObjIconTextM":null}]
// My attempt:
var filledProps = asset.map(el => {
if (Object.keys(el)) { // check if object property value is not null
return el;
};
});
console.log(filledProps);
But I get the same object properties back. What am I missing?
It sounds like you want to create a new array with new objects that only have the properties that aren't null from the original. Is so, map is where you want to start, but Object.keys(el) is always truthy, since it returns an array of property names. You're close though:
var asset = [{"ObjId":177791,"ObjCreditlineM":"DEU","ObjReprorechtM":null,"ObjKommentarM":null,"ObjZustandM":null,"ObjReserve01M":null,"ObjReserve02M":null,"ObjFeld01M":null,"ObjFeld02M":null,"ObjFeld03M":null,"ObjFeld04M":"Foto","ObjFeld05M":null,"ObjFeld06M":null,"ObjFeld07M":null,"ObjFeld01S":null,"ObjFeld02S":null,"ObjFeld03S":null,"ObjFeld04S":null,"ObjFeld05S":null,"ObjFeld06S":null,"ObjFeld07S":null,"ObjFeld01F":0,"ObjFeld02F":0,"ObjFeld01D":null,"ObjFeld02D":null,"ObjInv01S":null,"ObjInv02S":null,"ObjInv03S":null,"ObjInv04S":null,"ObjInv05S":null,"ObjInv06S":null,"ObjDinId":0,"ObjReferenz01Id":null,"ObjReferenz02Id":null,"ObjTransferId":null,"ObjGesperrtS":null,"ObjIconTextM":null}]
// Use `map` to get a new array with new objects
var filledProps = asset.map(el => {
// Loop the property names of `el`, creating a new object
// with the ones whose values aren't `null`.
// `reduce` is commonly used for doing this:
return Object.keys(el).reduce((newObj, key) => {
const value = el[key];
if (value !== null) {
newObj[key] = value;
}
return newObj;
}, {});
});
console.log(filledProps);
What am I missing?
Since if (Object.keys(el)) is always truthy, your code was just always returning el unchanged. It wasn't creating a new object, or deleting properties with null values from the original object.
The above creates new objects, but if you like, you can just delete properties from the originals that have null values instead:
var asset = [{"ObjId":177791,"ObjCreditlineM":"DEU","ObjReprorechtM":null,"ObjKommentarM":null,"ObjZustandM":null,"ObjReserve01M":null,"ObjReserve02M":null,"ObjFeld01M":null,"ObjFeld02M":null,"ObjFeld03M":null,"ObjFeld04M":"Foto","ObjFeld05M":null,"ObjFeld06M":null,"ObjFeld07M":null,"ObjFeld01S":null,"ObjFeld02S":null,"ObjFeld03S":null,"ObjFeld04S":null,"ObjFeld05S":null,"ObjFeld06S":null,"ObjFeld07S":null,"ObjFeld01F":0,"ObjFeld02F":0,"ObjFeld01D":null,"ObjFeld02D":null,"ObjInv01S":null,"ObjInv02S":null,"ObjInv03S":null,"ObjInv04S":null,"ObjInv05S":null,"ObjInv06S":null,"ObjDinId":0,"ObjReferenz01Id":null,"ObjReferenz02Id":null,"ObjTransferId":null,"ObjGesperrtS":null,"ObjIconTextM":null}];
asset.forEach(el => {
Object.keys(el).forEach(key => {
if (el[key] === null) {
delete el[key];
}
});
});
console.log(asset);
Two aspects of that to call out, though:
It modifies the original objects (and thus, in a way, the original array).
When you delete a property from an object, it can have an impact on the performance of property lookup on that object afterward. 99.999% of the time you don't care, but it's there.
asset = [{"ObjId":177791,"ObjCreditlineM":"DEU","ObjReprorechtM":null,"ObjKommentarM":null,"ObjZustandM":null,"ObjReserve01M":null,"ObjReserve02M":null,"ObjFeld01M":null,"ObjFeld02M":null,"ObjFeld03M":null,"ObjFeld04M":"Foto","ObjFeld05M":null,"ObjFeld06M":null,"ObjFeld07M":null,"ObjFeld01S":null,"ObjFeld02S":null,"ObjFeld03S":null,"ObjFeld04S":null,"ObjFeld05S":null,"ObjFeld06S":null,"ObjFeld07S":null,"ObjFeld01F":0,"ObjFeld02F":0,"ObjFeld01D":null,"ObjFeld02D":null,"ObjInv01S":null,"ObjInv02S":null,"ObjInv03S":null,"ObjInv04S":null,"ObjInv05S":null,"ObjInv06S":null,"ObjDinId":0,"ObjReferenz01Id":null,"ObjReferenz02Id":null,"ObjTransferId":null,"ObjGesperrtS":null,"ObjIconTextM":null}]
var filledProps = asset.map(el => {
var obj = {};
for(var prop in el) {
if(el[prop] !== null) {
obj[prop] = el[prop];
}
}
return obj;
});
console.log(filledProps);
I have an issue that I am struggling to grasp. Any help would be greatly appreciated.
I have an Object, and I assign the current object state to a property on the current object.
example below:
var product = {
ropeType: 'blah',
ropePrice: 'blah',
ropeSections: {
name: 'blaah',
price: 'blaah'
},
memory: false
}
product.memory = product;
Now when I look at the product object within the console I get a inifinite recursion of Product.memory.Product.memory.Product....
screenshot below:
I know its something to do with that an object references itself, but I cannot seem to grasp the concept. Could someone explain?
The reason I am trying to do something like this is to save in local storage the current state of the object.
I hope I have made sense.
I assign the current object state to a property on the current object.
No, you created a property that referred to itself.
If you want to save the current state of the property then you need to clone the object.
If you want to create a (shallow) copy of an object then you can use:
function clone(obj) {
if(obj === null || typeof(obj) !== 'object' || 'isActiveClone' in obj)
return obj;
var temp = obj.constructor();
for(var key in obj) {
if(Object.prototype.hasOwnProperty.call(obj, key)) {
obj['isActiveClone'] = null;
temp[key] = obj[key];
delete obj['isActiveClone'];
}
}
return temp;
}
[code taken from here - and modified slightly to do a shallow copy rather than recursive deep copy]
then do:
product.memory = clone( product );
You may find you get the issues with recursion if you clone it a second time and it copies the product.memory along with the rest of the object. In that case just delete product.memory before doing subsequent clones.
Something like:
function saveCurrentState( obj ){
if ( 'memory' in obj )
delete obj.memory;
obj.memory = clone( obj );
}
Aside
If you want a deep copy then you can do:
function deepCopy(obj){
return JSON.parse(JSON.stringify(obj));
}
[As suggested here - but note the caveats it has for Date objects]
you could do your idea by clone the current product into new. We've Object.keys to get all attribute of object. So here is my idea :
product = {
ropeType: 'blah',
ropePrice: 'blah',
ropeSections: {
name: 'blaah',
price: 'blaah'
},
memory: false
};
var keys = Object.keys(product);
var newProduct = {};
keys.forEach(function(key){
if(key === 'memory') return;
newProduct[key] = product[key];
});
product.memory = newProduct;
Instead of actually storing a reference to the object, you might want to transform that object's state. Maybe by cloning it onto a new object or possibly keeping it as a JSON string (which you'll want to do if you're using localStorage).
Since you will probably want to see the current state of the object whenever you check the memory property, you should make memory a function that does that transformation.
Maybe something like this:
var product = {
ropeType: 'blah',
ropePrice: 'blah',
ropeSections: {
name: 'blaah',
price: 'blaah'
},
memory: function() {
return JSON.stringify(this);
}
}
You can then call product.memory() and get its state in JSON.
This here is the problem:
product.memory = product;
You're assigning a reference to an object to itself. JavaScript passes objects by reference, so it's never going to store a clone of itself through assignment.
If you're looking to record modifications made to the object over time, the best way would be to use an array to hold cloned copies of it (or at least the properties that've changed).
To give you the quickest example:
var Product = function(){
};
var product = new Product();
product.history = [];
product.saveState = function(){
var changes = {};
for(var i in this){
/** Prevent infinite self-referencing, and don't store this function itself. */
if(this[i] !== this.history && this[i] !== this.saveState){
changes[i] = this[i];
}
}
this.history.push(changes);
};
Obviously, there're many better ways to achieve this in JavaScript, but they require more explanation. Basically, looping through an object to store its properties is inevitably going to trip up upon the property that they're being assigned to, so a check is needed at some point to prevent self-referencing.