Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Map.mergeDeep - how to avoid merging Lists within #762

Closed
ntgn81 opened this issue Jan 15, 2016 · 3 comments

Comments

Projects
None yet
4 participants
@ntgn81
Copy link

commented Jan 15, 2016

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
    // 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.

@leebyron

This comment has been minimized.

Copy link
Collaborator

commented Apr 16, 2016

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.

@leebyron leebyron closed this Apr 16, 2016

@joual

This comment has been minimized.

Copy link

commented Apr 22, 2016

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
}
@lucas-martinez

This comment has been minimized.

Copy link

commented Jan 15, 2018

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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.