12

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! ;)

mseimys
  • 588
  • 1
  • 7
  • 16
  • 1
    Why not using deepEqual: http://stackoverflow.com/questions/25456013/javascript-deepequal-comparison – Gavriel Jan 26 '16 at 10:41
  • 1
    Are you sure it gets rerendered? React works the way that when one single item in a state changes the whole new state gets compared with the whole old state. Therefore the state has been looped and you might receive some logs if you log something within that lifecycle. But if nothing DOM relevant has changed then react most likely won't rerender anything. Check the shouldComponentUpdate method. – noa-dev Jan 26 '16 at 10:43
  • Because the data is immutable.. – Dominic Jan 26 '16 at 10:45

2 Answers2

18

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
Henrik Andersson
  • 45,354
  • 16
  • 98
  • 92
1

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.

Brandon
  • 38,310
  • 8
  • 82
  • 87
  • That sounds like a good idea, but then my state would be stored in two places and I would have to synchronize it, using ... actions? State would look like: `{ selectedItems: [1], items: [ {id: 1, selected: true}, {id: 2, selected: false} ]}` After making state like that when handling select action I would need to update two state attributes, sounds easy, but I would have to do that **all the time**: when I delete an item, when I switch to next page, etc. This would cause a lot of extra logic all around the code :) – mseimys Jan 26 '16 at 21:03
  • 1
    Indeed, each reducer that you have that currently modifies the `items` property will also need to modify the `selectedItems` property if the action caused a change in the selection. Presumably you only have a handful of actions that are affected? A second option is to *remove the selected property* from the items and *only* store the selectedItems as a separate property in the store, either as a list or as a set: `{ selectedIds: [1,...], items: [{id:1, name:"foo"},...]}` or `{selectedIdSet: Set(1,...), items:{id:1,name:"foo"},...]}` – Brandon Jan 26 '16 at 21:14