Two immutable lists - how to make triple equality work? - javascript

Let's say we have an immutable object that is created using Facebook's great Immutable.js. I want to compare two lists that were produced using .map or .filter out of single source and make sure they are equal. It seems to me, that when using map/filter you are creating a new object that has nothing to do with a previous object. How can I make triple equality === work? Does it make sense at all?
var list = Immutable.List([ 1, 2, 3 ]);
var list1 = list.map(function(item) { return item; })
var list2 = list.map(function(item) { return item; })
console.log("LIST1 =", list1.toJS()) // [1, 2, 3]
console.log("LIST2 =", list2.toJS()) // [1, 2, 3]
console.log("EQUAL ===?", list1===list2); // false! Why? How?
You can play with it here: http://jsfiddle.net/eo4v1npf/1/
Context
I am building application using React + Redux. My state has one list that contains items, that have attribute selected:
items: [
{id: 1, selected: true},
{id: 2, selected: false},
{id: 3, selected: false},
{id: 4, selected: true}
]
I want to pass only selected ids to another container, so I tried it using simple connect:
function getSelectedIds(items) {
return items
.filter((item) => item.get("selected"))
.map((item) => item.get("id"));
}
export default connect(
(state: any) => ({
ids: getSelectedIds(state.get("items"))
})
)(SomePlainComponent);
Problem is, if I set additional attributes:
{id: 1, selected: true, note: "The force is strong with this one"}
This causes state to change and SomePlainComponent to rerender, although the list of selected Ids is exactly the same. How do I make sure pure renderer works?
Edit with some additional info
For react pure rendering I was using mixin from react-pure-render:
export default function shouldPureComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState);
}
As it is not aware of props that could be immutable, they are treated as changed, i.e.
this.props = {
ids: ImmutableList1
}
nextProps = {
ids: ImmutableList2
}
Although both attributes ids are equal by content, they are completely different objects and do not pass ImmutableList1 === ImmutableList2 test and shouldComponentUpdate returns true. #Gavriel correctly pointed that deep equal would help, but that should be the last resort.
Anyway, I'll just apply accepted solution and problem will be solved, thanks guys! ;)

You can never have strict equality of immutable structures since an Immutable.js object, inherently, is unique.
You can use the .is function which takes two immutable objects and compares the values within them. This works because Immutable structures implement equals and hashCode.
var map1 = Immutable.Map({a:1, b:1, c:1});
var map2 = Immutable.Map({a:1, b:1, c:1});
console.log(Immutable.is(map1, map2));
// true

If you want to keep your component pure and working with === then you can also denormalize your Redux state and store the selectedIds as a property in the store. Only update this list when an action occurs that adds/removes a selected item or toggles an item selection, but not when other arbitrary properties of the item are updated.

Related

React updating a state array that is nested within array of objects

So I'm feeling somewhat confident on updating the array of objects when it's just array of objects using map and the spread function. However, where I'm stuck on is updating an array that is within array of objects.
Here's the example.
I have a seperate state to choose which index
const [option, setOption] = useState(0);
I initialize the state with array of objects
const [parts, setParts] = useState([
{bodyPart: "upperLip", price: 1000, active: [false, false, false]},
{bodyPart: "chin", price: 1000, active: [false, false, false]},
])
Basically, I want to update the array that is nested in array of objects when the description of bodyPart matches up.
const handleOnClick = (bodyPart) => {
parts.map((part) => {
if (part.bodyPart === bodyPart){
return {...part, active[option]=true}
} else {
//do nothing
}
})
}
I know that return {...part, active[option]=true} part is incorrect. What would be the proper way of updating this piece within the state?
I've tried to set it to true like I would for an array, but I'm not too sure anymore.

Add or Remove element from array in nested Object in React hooks

In this below example, I want add or remove the products array elements from object3 and need to updated in react hooks state.
I have tried with filter method inside setter for deleting an element but it won't work. can anyone pls help to do with efficiently.
const myValue = {
object1: {},
object2: {},
object3: {
products: [{
name: 'Fruits',
id: '1'
},
{
name: "Vegtables",
id: '2'
}],
number: 1
}
}
You can extract out the object3 as another variable, do whatever operations you want to do and set state again combining it with the current state value.
const {object3} = myValue;
// for example if you want to filter based on condition for id
const modifiedProducts = object3.products.filter((item) => item.id > '1');
setMyValue({
...myValue,
object3: {
...object3,
products: modifiedProducts
}
})
I would also suggest making multiple states for each object if all objects are independent from each other. Here if some component only depends on object1, ideally it should not re render when object 3 updates.

Extract value of property from object in a Set?

For the example, SomeItem is the model for an object (would be modeled as an interface in Typescript or you can just imagine that there is an item with the form of SomeItem if we are in untyped land.
Say I have a Set: mySet = new Set([{item: SomeItem, selected: true}, ...]).
And I want to check if itemA: SomeItem is selected or not.
What is the cleanest way to do this?
This did not work:
const isSelected = mySet.has({item: itemA, selected: true});
Nor did this:
const isSelected = Array.from(mySet).includes({item: itemA, selected: true});
I'm assuming the above two did not work because it is trying to compare the objects by reference, rather than value.
This does work:
let isSelected: boolean;
mySet.forEach(state => {
if (state.item === itemA) {
isSelected = state.selected;
}
});
But my gut tells me there is a correct way to do this.
So,
How do I extract the value of a property of an object in a Set?
Comparing two objects with the same properties returns true only if they have the same reference, I would suggest to compare their properties as the properties are primitive values..
The array some method can be used to filter if the set contains an specific object
let mySet = new Set([{item: 'SomeItem', selected: true}]);
let itemA = "SomeItem";
let isSelected = Array.from(mySet).some(element => element.item === itemA);
console.log(isSelected);
Let's take a look at it this way, Sets mainly return an iterable. Sure, they're hashed in order, as is a Map, but from the looks of your data structure, a Map would benefit you more here.
const x = new Set([
{ "foo": 1, selected: true },
{ "bar": 1, selected: false },
{ "baz": 1, selected: false },
{ "barf": 1, selected: false },
]);
Now, to get what you're looking for, you'll need as you did, convert to an array using Array.from (or [...x] spread it) and iterate, finding they key.
Now, as a Map:
const y = new Map();
y.set("foo", { selected: true });
y.set("bar", { selected: false });
y.set("baz", { selected: false });
y.set("barf", { selected: false });
With this, you simply change the structure slightly to give item1 or whatever you use the Map key, and set whatever elements you want.
y.has("foo"); // true
y.get("foo").selected; //true
So if you wanted here, it's much easier to grab the iterable key name and get which Map index has the property you want

Adding one element in array in React-Redux

I have a project, where I use react-redux, and I have a reducer, which by idea should add one element in array and return new array. How I can do this?
/*---- Reducer ----*/
case CHANGE_EVENT_USERS:
return { ...state, users: payload };
/*---- Here's my hopeless tryings ----*/
userClickHandler() {
const { id, homeFloor, avatarUrl, login } = this.props;
const user = { id, homeFloor, avatarUrl, login };
this.props.changeEventUsers([...user]); // []
this.props.changeEventUsers(this.props.event.users.push()); // number
}
I'm not sure if I understand you correctly but from my understanding the solution to your problem would look something like this:
case CHANGE_EVENT_USERS:
return { ...state, users: [ ...state.users, action.payload ] };
I like better the syntax of concat.
In your reducer do:
case CHANGE_EVENT_USERS:
return users.concat(action.payload);
Do the add directly in the reducer.
From your component
this.props.changeEventUsers(newUser); // add the new user
In the reducer
return { ...state, users: [...state.users, payload] };
I made the assumption that "payload" contains the info coming from the action and the users array is by default initialised with an empty array value []
Use concat()
const initialArray = [1, 2, 3];
const elemenToAdd = 4;
const newArray= initialArray.concat([elementToAdd]);
[1, 2, 3, 4]
Notice I'm using const here to emphasize that the initial array was not mutated.
The great thing about the method above, it's that it can be used to chain operations together.
result = initialArray.concat(..).filter(..).concat(..);
(where .. represents skipped code details)
You can also use concat by passing in arrays as parameters:
newArray = concat(initialArray, [elementToadd])
Or use es7 spread operator syntax for array concatenating:
newArray = [...initialArray, elementToAdd, ...[5], [6] ];
[1, 2, 3, 4, 5, [6] ]
Use ... to send individual elements of the supplied array to be elements in the new array; without the dots, the supplied array is itself the element.
So in the your case, the line in question could be written as:
I found my solution:
this.props.changeEventUsers([...this.props.event.users, user]);

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.

Categories