Immutable-js: Map.mergeDeep - how to avoid merging Lists within

Created on 15 Jan 2016  路  3Comments  路  Source: immutable-js/immutable-js

I would like to use mergeDeep to merge my deeply nested state object, but not try to merge the Lists whenever it sees them

Simple example. More detailed sample below.

var originalData = {person: { name: 'John', skills: ['drinking', 'eating']}};
var originalState = Immutable.fromJS(originalData);
var updatedData = {person: {name: 'John Doe', skills: ['sleeping']}};
var updatedState = originalState.mergeDeep(updatedData);
updatedState.toJS().person.skills; // ["sleeping", "eating"]

I need the arrays to be ovewritten, not merged. I understand that mergeDeep is designed that way. I am just wondering if there's an easier way to achieve what I need. Right now I am doing this:

  1. Traverse updatedData object, find properties that are Array.isArray
  2. When found

    • get the path to the current entry

    • use setIn(), passing that array

    • delete that array node from updatedData to lighten the load for mergeDeep

js // mergeDeep that replaces arrays with instead of merging function mergeDeepReplaceArrays (state, updatedData) { const replaceArrays = (data, basePath) => { basePath = basePath || []; _.forIn(data, (v, k) => { const path = basePath.concat([k]); if (Array.isArray(v)) { if (state.hasIn(path)) { state = state.setIn(path, Immutable.fromJS(v)); delete data[k]; } } else if (_.isObject(v)) { replaceArrays(v, path); } }); }; replaceArrays(updatedData); return state.mergeDeep(updatedData); }

This definitely seems hacky and weird. Would be nice, I think, to have a hook into mergeDeep, similar to mergeDeepWith, except for when the optional merger will be called for every node, giving original/target rather than just on the conflicts.

Edit: Updated the code to use withMutations, making it a bit faster, but for some weird reasons, my hacky/crappy code is faster than plain mergeDeep, according to jsPerf: http://jsperf.com/withmutations-test

More detailed sample

// Initial entities
var entities = Immutable.fromJS({
  products: {
    '1': {
      name: 'iPhone',
      categories: ['tech', 'phone', 'mobile']
    },
    '2': {
      name: 'Apple',
      categories: ['food', 'fruit']
    },
    '3': {
      name: 'Jacket X',
      categories: ['clothing']
    }
  },

  // list of product ids
  carts: {
    '1': {
      name: 'First Cart',
      items: [1, 2, 3]
    },
    '2': {
      name: 'Second Cart',
      items: [3]
    }
  }
})

A change occured to some items:

  • iPhone

    • Has tech category removed

  • First Cart:

    • Item 2 is removed

    • Name is changed

var entitiesUpdate = {
  products: {
    '1': {
      name: 'iPhone',
      categories: ['phone', 'mobile']
    }
  }
  carts: {
    '1': {
      name: 'First Cart updated',
      items: [1, 3]
    }
  }
}

I need the arrays at products['1'].categories and carts['1'].items to be replaced to properly reflect the update, not merged into ['phone', 'mobile', 'mobile'] and [1, 3, 3] respectively.

Most helpful comment

mergeDeepWith actually only interacts when 2 elements are non-iterable I think.

I got it to work with mergeWith as follow

import { List } from 'immutable'
const isList = List.isList
export default function merger(a, b) {
  if (a && a.mergeWith && !isList(a) && !isList(b)) {
    return a.mergeWith(merger, b)
  }
  return b
}

All 3 comments

Best to ask this type of question on Stack Overflow where there is more traffic. https://stackoverflow.com/questions/tagged/immutable.js?sort=votes

The answer likely includes mergeDeepWith which allows for a custom merge function.

mergeDeepWith actually only interacts when 2 elements are non-iterable I think.

I got it to work with mergeWith as follow

import { List } from 'immutable'
const isList = List.isList
export default function merger(a, b) {
  if (a && a.mergeWith && !isList(a) && !isList(b)) {
    return a.mergeWith(merger, b)
  }
  return b
}

I was looking for something similar. I will just place my modification to @ntgn81 code here.

import { fromJS, List, Map, Set } from 'immutable';
import { Path } from '../types';

// mergeDeep that merges arrays as sets instead or overwriting it. If only fromJS converted to Set instead of List...
export function mergeDeepArrays (state: any, value: any): any {

    const mergeArrays = (data: Map<string, any>, basePath: Path) => {

        let next: Map<string, any> = data;

        data.forEach((v, k) => {
            const path = basePath.concat([<string> k]);
            if (List.isList(v)) {
                if (state.hasIn(path)) {
                    const current = state.getIn(path);

                    if (List.isList(current) && !current.isEmpty()) {
                        state = state.setIn(path, Set(current).merge(v));
                        next = next.deleteIn(path);
                    }
                }
            } else if (Map.isMap(v)) {
                next = next.setIn(path, mergeArrays(v, path));
            }
        });

        return next;
    };

    const updatedData = mergeArrays(fromJS(value), []);

    return state.mergeDeep(updatedData);
}

export default mergeDeepArrays;
Was this page helpful?
0 / 5 - 0 ratings

Related issues

chandlerprall picture chandlerprall  路  3Comments

Delapouite picture Delapouite  路  3Comments

OliverJAsh picture OliverJAsh  路  4Comments

Daniel15 picture Daniel15  路  4Comments

jshthornton picture jshthornton  路  3Comments