Add nested property in Redux reducer - reactjs

I've been a few days trying to get my head around Redux to be used with my Reactjs project. I'm working on a reducer and trying to add a nested property into state. To do this, I'm using spread operator at different levels. The problem is that initially the state slice this reducer manages is empty and therefore I run into the following problem... taking this as an example of what I'm trying to do:
var foo = {}; // Initially the state slice is an empty object.
var bar = {...foo,
1: {
...foo[1],
2: {
...foo[1][2],
baz: 100 // New property I'm trying to add.
}
}
};
console.log(bar);
(property keys 1 and 2 are two variables provided to the reducer in my actual app)
I you run the above piece of code it will yield en error saying Cannot read property '2' of undefined at line ...foo[1][2],. Alright, foo[1][2] is not defined yet, but how am I supposed to accomplish this? Maybe I'm missing something very basic...

something like that works for your example but it seems pretty hacky.
bar = {
...foo,
1: {
...foo[1] ? foo[1] : {},
2: {
...foo[1] && foo[1][2] ? foo[1][2] : {},
baz: 100 // New property I'm trying to add.
}
}
}

Related

Why doesn't useState work with deeply nested objects and arrays in them?

In my use case I have an array of characters, each character has multiple builds, and each build has a weapons string, and artifacts string. I'm making a tool to select portions of each string and assign them to a value, e.g. assigning index 3-49 of weapons to a specific weapon.
const [characterIndices, setCharacterIndices] = useState<
{ builds: { weaponIndices: SE[]; artifactSetIndices: SE[] }[] }[]
>([
...characters.map((char) => {
return {
builds: [
...char.builds.map((_build) => {
return {
weaponIndices: [],
artifactSetIndices: [],
};
}),
],
};
}),
]);
The SE type is as follows:
type SE = { start: number; end: number; code: string };
//start and end are the respective start and end of selected text
//code is the specific artifact or weapon
The weaponIndices and artifactSetIndices basically hold the start and end of selected text in a readonly textarea.
I have a function to add a SE to either weaponIndices or artifactSetIndices:
const addSE = (
type: "weaponIndices" | "artifactSetIndices",
{ start, end, code }: SE,
characterIndex: number,
buildIndex: number
) => {
let chars = characterIndices;
chars[characterIndex].builds[buildIndex][type].push({ start, end, code });
setCharacterIndices((_prev) => chars);
console.log(characterIndices[characterIndex].builds[buildIndex][type]);
};
I think that using a console log after using a set function isn't recommended, but it does show what it's intended to the weaponIndices, or artifactSetIndices after an entry is added.
Passing the addSE function alongside characterIndices to a separate component, and using addSE, does print the respective indices after adding an entry, but the component's rendering isn't updated.
It only shows up when I "soft reload" the page, when updating the files during the create-react-app live reload via npm run start.
In case you are confused about what the data types are, I've made a github repo, at https://github.com/ChrisMGeo/ght-indexer/tree/main/src at src/data.json. That JSON file describes what the character data looks like, including the builds, and each build's weapons and artifacts(called artifact_sets in the JSON)
Looks to me you are not updating the state at all.
Here you are just storing the same object reference that you already have in state into a new variable chars.
let chars = characterIndices;
chars now holds reference to a same object as characterIndices.
Here you are mutating that same object
chars[characterIndex].builds[buildIndex][type].push({ start, end, code });
And here you are updating the state to the same object that is already in the state. Notice that no state update here occurs.
setCharacterIndices((_prev) => chars);
Object you have in state is mutated, but you did not "change" the value of the state, thus no component re-render.
What you could maybe do is create a copy of the object, mutate that and update the state. just change chars assignment like this:
let chars = {...characterIndices};
React often compares values using Object.is() only to a single level of nesting (the tested object and its children).
It will not re-render if the parent is found equal, or if all the children are found equal.
React then considers that nothing has changed.
In your implementation, even the first top-level check will immediately fail, since Object.is(before, after) will return true.
You could use an Immutable objects approach to eliminate this concern when setting a new state (either directly through spreading values or with a support library such as Immer).
For example instead of setting the values within the object...
myObj.key = newChildObj
...you would make a new object, which preserves many of the previous values.
myObj === {...myObj, key: newChildObj}
This means that every changed object tree is actually a different object (with only the bits that haven't changed being preserved).
To read more about this see https://javascript.plainenglish.io/the-effect-of-shallow-equality-in-react-85ae0287960c

Cannot assign to read only property of Object in TypeScript

can anyone explain to me please why and how this might happen:
I have a typescript app with Zustand state management.
Somewhere during the app I am updating certain elements by extracting them from the state and cloning via simple Object.Assign :
let elemToUpdate = Object.assign({},story?.content?.elementsData[nodeId]);
console.log(elemToUpdate);
if(elemToUpdate) {
if(elemToUpdate.title) elemToUpdate.title[editorLang] = newName;
else elemToUpdate.title = {[editorLang]:newName} as TextDictionary;
updateElement(nodeId,elemToUpdate);
}
Now the interesting part is on my first try the update goes through without fail, but the next object I am trying to update fails with the following message:
Tree.tsx:39 Uncaught TypeError: Cannot assign to read only property 'en' of object '#<Object>'
I can't understand WHY the first one comes through, but the second gets blocked.
(I know HOW to fix it, need to do deep clone, I just want to understand WHY)
Thanks
First, let's start from why some objects in your code are readonly. Based on what you described in the question, you use a Zustand state manager. Such managers traditionally wraps you stored data to readonly objects to prevent it's manual mutation (expecting, you will change the state only via built-in mechanisms), to guarantee data stability. So, if the story?.content?.elementsData[nodeId] is the Zustand state object, it self and all it's nested objects are converted to readonly.
Second, let's define, which objects will be blocked. I see at least two objects here: elemToUpdate: { ..., title: { [lang]: string }} (elemToUpdate and it's title). Both will be converted to readonly.
Third, you use Object.assign({}, ...) which creates a new object (new reference) and clones all properties of the source object. It happens only for first level of properties, no deep clone. So, as the title is a reference to another object, it will be cloned as is and in the new object it still leads to the existing { [lang]: string } object. There are several way to solve that: 1) deep clone as you mentioned; 2) manually clone title property, for instance {..., title: { ... elemToUpdate.title }} or via Object.assign
But I would suggest don't mutate you object this way. Probably, your entire algorithm has some architectural issues in general.
That is expected because in the first case you are not assigning value to the title you are only changing the value of the title property. In the second case, you are reassigning the value of the title property,
it's the read-only value you cant change it. Let's understand with a simple example
Javascript: Only for example not related to problem
const user = {
name: 'John',
}
user.name = "Pete"; // This works
const user = {
name: 'John',
}
user = { name: 'Pete'} // This doesn't work
Typescript:
const user: Readonly<{
a: {
name: string
}
}> = {
a:{ name: 'John',}
}
user.a.name = "Pete"; // This works
user.a = { name: 'John',} // not work
The same is happening there, Typescript does not check deep Readonly prop. check here

Add / Update Deep Array of Objects in React / Redux

In my react redux reducer, how do I add or update a deep nested array of objects with the spread operator? I’m having trouble getting the syntax right.
My state is roughly in this shape:
state: {
fields...
ups: {
fields...
ftr: {
fields...
arts: [
{
artId: 12,
name: ‘joe’,
phones: [
{
phoneId: 58,
number: ‘nnn’
}
]
}
]
}
}
}
I come in with artId and either with an empty phone object or an existing one to add/update the phones array. Likewise the same for the parent arts. Can't do byId and too late to switch to 'normilizr`.
To start, it's a little difficult comprehending the exact issue you are having.. It would be extremely useful if you could supply some of the code you are using to update your state inside of your reducer, not just the shape of your state.. In order to accurately assist, we need to see more code..
With that being said, if I am understanding this correctly, when you are updating state in a reducer, you need to make a deep copy first.. or are you trying to use the spread operator like this: fields...?...or is that just for brevity?
The spread operator offers a shallow copy, so you'll have to do something like this to get a deep[er] copy:
case types.SOME_CONSTANT: {
let _newstate = JSON.parse(JSON.stringify(state));
_newstate.some.deep.nested.property = action.payload;
return _newstate;
}

Immutable js UpdateIn is storing number instead of object

I am trying to update a immutablejs object;
//action.vals = {element: "p", type: "text", content: "test", className: "paragraph-topic"}
return state
.updateIn(['contents'], list => list.push(action.vals)) //<<<<<THis is failing
.set('loading', false)
.set('error', false)
break;`
But instead it is storing 10 an integer .
I am really confused and i am in need of help suggestion.
Here is my full code
Any help is much appreciated! I am going nuts due to lack of proper usage documentation
I've noticed that on the LOAD_DATA reducer, you're setting contents to become an array instead of an immutable List:
case LOAD_DATA:
return state
.set('loading', true)
.set('error', false)
.setIn(['contents'], [])
This would cause quite a few problems. What's happening is that you're using the vanilla JS push function on your update, and that returns the length of the array. So I'm supposing contents has 10 elements?
You just need to change the LOAD_DATA reducer to have this instead:
.setIn(['contents'], fromJS([]))
or
.setIn(['contents'], new List())
If using List(), that has to be explicitly imported as well!
Do some thing like this using ....
.updateIn(['contents'], list => [...list, action.vals])

Mutating array within an array (Polymer iron-list)

I currently have an iron-list within another iron-list. The parent's data comes from a firebase-query element, and the child's data is computed from each parent item. The db structure and code looks a bit like this:
DB: [
category1: [
itemId1: {
price: 10,
title: "title"
}
]
]
<iron-list id="categoryList" items="{{categories}}" multi-selection as="category">
<template>
<div class="category-holder">
<iron-list id="{{category.$key}}" items="{{_removeExtraIndex(category)}}" as="item" selection-enabled multi-selection selected-items="{{selectedItems}}" grid>
<template>
<div class$="{{_computeItemClass(selected)}}">
<p>[[item.title]]</p>
<p>[[item.price]]</p>
</div>
</template>
</iron-list>
</div>
</template>
</iron-list>
After selecting any number of items, the user can tap on a fab to batch edit the price. This is where I'm having issues. I can't figure out how to access the correct child iron-list in order to call list.set...I'm currently trying the following very nasty method:
var categories = this.$.categoryList;
var categoryItems = categories.items;
(this.selectedItems).forEach(function(item) {
var index = item.itemId;
categoryItems.forEach(function(itemList, categoryIndex) {
if (itemList[index]) {
categories.set('item.' + categoryIndex + '.price', 10);
}
}, this);
}, this);
I'm iterating over the selected items in order to extract the item index and then iterating over the parent iron-list data (categoryItems) in order to check if the given item exists in that subset of data. If so, then I use the category index and attempt to call set on the parent iron-list using the given path to access the actual item I want to edit. As expected, this fails. Hopefully I've made myself clear enough, any help would be appreciated!
EDIT #1:
After much experimenting, I finally figured out how to correctly mutate the child iron-list:
(this.selectedItems).forEach(function(item) {
var list = this.$.categoryList.querySelector('#' + item.category);
var index = list.items.indexOf(item);
list.set(["items", index, "price"], 30);
}, this);
A couple of things worth noting. I'm using querySelector instead of the recommended this.$$(selector) because I keep running into a "function DNE" error. But now I have another problem...after calling the function, the value gets updated correctly but I get the following error:
Uncaught TypeError: inst.dispatchEvent is not a function
Here's a picture of the full error message:
I see the light, hopefully someone can help me out!
OK, I'll take a shot at this. I think the following happens, and I guess this based on how dom-repeat works:
var categories = this.$.categoryList;
var categoryItems = categories.items;
You take the variable that the iron-list is based on, but setting one array to another just creates a reference in javascript. As soon as you update categoryItems, you also update this.$.categoryList.items. When you later sets the new value, iron-list will do a dirty check and compare all subproperties, and because they are equal (because ... reference), the iron-list wont update the dom.
What you should do is to make sure it's a totally new copy and the way of doing that is to use JSON.parse(JSON.stringify(myArray)).
Further on, one major flaw I see in your code is that you're using querySelector to select an element, and then manipulate that. What you should do is to use this.categories and only that variable.
So your method should look something like:
// Get a freshly new array to manipulate
var category = JSON.parse(JSON.stringify(this.categories);
// Loop through it
category.forEach(category) {
// update your categoryList variable
}
// Update the iron list by notifying Polymer that categories has changed.
this.set('categories', category);

Resources