Menu
Menu Sheet Overlay
Search
Search Sheet

Immutable.js

    In Mobify’s Progressive Mobile Web system, we use Immutable.JS for the Redux store. Immutable.JS provides a set of immutable data structures that we can use to store our application data. These data structures cannot be modified, but they provide many methods for creating modified copies.

    Redux requires that the objects making up the store are never mutated, only replaced with modified copies. With Immutable objects, we do not need to worry about introducing bugs by violating this constraint. This document describes the subset of the Immutable.JS API that is generally used in the Redux reducers, with examples.

    For the more complex functions, equivalents will be given in terms of more simple functions, when possible, to illustrate their operation.

    Immutable Objects #

    We use two kinds of Immutable objects in our current code, Map and List. These correspond to the basic Javascript Object and Array types, respectively.

    For the most part, Map and List behave identically, with the Map having string keys and the List having integer keys. Differences between their APIs will be noted below.

    List also implements equivalents of the standard Array methods, such as .map, .filter, and .reduce. These equivalent methods provide the same API, but return Immutable List objects rather than plain Javascript arrays.

    The fromJS function #

    See also: fromJS()

    Immutable provides a helper function Immutable.fromJS for building a complex Immutable structure from basic Javascript literals. It will translate the object or array passed to it into Immutable Map and List objects, including any object or array members. Always use fromJS for constructing initial states, unless the initial state is empty (in which case it can be Immutable.Map() or Immutable.List()).

    Example:

    const initialState = Immutable.fromJS({
        title: 'Initial State Example',
        contents: ['One', 2, true],
        link: {
            text: 'Example Link',
            href: 'https://www.mobify.com'
        }
    })
    
    // is equivalent to
    
    const initialState = Immutable.Map({
        title: 'Initial State Example',
        contents: Immutable.List(['One', 2, true]),
        link: Immutable.Map({
            text: 'Example Link',
            href: 'https://www.mobify.com'
        })
    })
    

    Equality Testing #

    See also: is()

    Except in very limited cases, Immutable.JS is not able to ensure that two objects with the same contents are the same, identical object. This means that we cannot use the === operator to compare two Immutable objects. Instead, Immutable provides the is helper function to determine equality of two Immutable objects.

    This is a deep comparison of the contents at all levels of the object, but it is nearly as efficient as a direct identity comparison. Each Immutable object contains a hash code of its contents; the is function compares this between the two objects to determine equality.

    Example:

    const state1 = Immutable.fromJS({
        details: {
            equalToFirst: true
        }
    })
    
    const state2 = Immutable.fromJS({
        details: {
            equalToFirst: true
        }
    })
    
    const state3 = Immutable.fromJS({
        details: {
            equalToFirst: false
        }
    })
    
    Immutable.is(state1, state2) // true
    Immutable.is(state1, state3) // false
    

    Basic Operations #

    These are the basic operations for accessing and modifying the contents of a Map or List. The modification methods return new Map or List objects with the relevant information changed

    .get #

    See also: Map, List

    Immutable.JS objects do not use the usual [] operator for accessing their members. Instead, they use the .get method. It takes two parameters:

    Example:

    const state = Immutable.fromJS({
        title: 'Get operations',
        isImmutable: true
    })
    
    console.log(state.get('title'))
    console.log(state.get('body', 'Empty!'))
    
    // Prints:
    // Get operations
    // Empty!
    

    .set #

    See also: Map, List

    The .set method is the most basic way to set a property on an Immutable object. It returns a new Immutable object with the given key set to the given value. It takes two required arguments:

    Examples:

    const state = Immutable.Map({
        data: '1 2 3 4'
    })
    
    const state2 = state.set('data', '5 6 7 8')
    // state2 = Immutable.Map({
    //    data: '5 6 7 8'
    // })
    
    const state3 = state.set('tagged', true)
    // state3 = Immutable.Map({
    //    data: '1 2 3 4',
    //    tagged: true
    // })
    

    .update #

    See also: Map, List

    The .update method allows changing a value in a container in a way that depends on the previous value. This can be very helpful when arithmetic is necessary, or when a member array needs to be mapped or filtered.

    It takes two required arguments:

    Equivalent:

    state.update('count', (count) => count + 2)
    
    // is equivalent to
    
    state.set('count', state.get('count') + 2)
    

    Examples:

    const state = Immutable.fromJS({
        items: ['toothbrush', 'toothpaste', 'passport', 'wallet']
    })
    
    const state2 = state.update(
        'items',
        (items) => items.filter((item) => item.startsWith('t'))
    )
    // state2 = Immutable.fromJS({
    //     items: ['toothbrush', 'toothpaste']
    // }
    

    .delete #

    See also: Map, List

    The .delete method simply removes a key from the object. Generally this is only useful for Map objects, but we can remove objects from a List with it as well.

    It takes a single argument:

    Examples:

    const state = Immutable.fromJS({
        name: 'Raymond Holt',
        title: 'Captain'
    })
    
    const state2 = state.delete('title')
    // state2 = Immutable.Map({
    //     name: 'Raymond Holt'
    // }
    

    Path Operations #

    The basic operations are very awkward for dealing with nested structures, as they would require an update operation at every level of the tree. Instead, Immutable provides a family of functions for performing an operation at a specific path within the tree. These functions are named identically to their single-level equivalents, but with the suffix In, e.g. setIn.

    Each of these operations takes a path as input, which is an array (or Immutable List) of keys for successive levels of the tree. For example, given the following object:

    const state = Immutable.fromJS({
        users: [
            {
                username: 'mobify_qa'
                name: 'Mobify QA Account'
            }
        ]
    })
    

    then the path to the value 'mobify_qa' is

    ['users', 0, 'username']
    

    .getIn #

    See also: Map, List

    The .getIn method gets a value at a path within an object, avoiding a string of .get calls. It takes two arguments:

    Equivalent:

    state.getIn(['items', 2, 'title'])
    
    // is equivalent to
    
    state.get('items').get(2).get('title')
    

    Example:

    const state = Immutable.fromJS({
        users: [
            {
                username: 'mobify_qa'
                name: 'Mobify QA Account'
            }
        ]
    })
    
    state.getIn(['users', 0, 'name']) // === 'Mobify QA Account'
    state.getIn(['users', 0, 'email'], 'nobody@nowhere.com') // === 'nobody@nowhere.com'
    

    .setIn #

    See also: Map, List

    The .setIn method sets a value deep inside an Immutable.JS tree. This creates new versions of each containing object as the change propagates back to the root. It takes two arguments:

    Equivalent:

    state.setIn(['items', 2, 'title'], 'Immutable.JS Guide')
    
    // is equivalent to
    
    state.update('items',
        (items) => items.update(
            2,
            (item) => item.set('title', 'Immutable.JS Guide')
        )
    )
    

    Example:

    const state = Immutable.fromJS({
        productInfo: {
            price: '10.99€'
        }
    })
    
    const state2 = state.setIn(['productInfo', 'price'], '$20.00')
    // state2 = Immutable.Map({
    //     productInfo: {
    //         price: '$20.00'
    //     }
    // })
    

    .updateIn #

    See also: Map, List

    The .updateIn method updates a value deep in an Immutable.JS tree, similar to the .update function. It takes two arguments:

    Equivalent:

    state.updateIn(['items', 2, 'count'], (count) => count + 1)
    
    // is equivalent to
    
    state.updateIn('items',
        (items) => items.updateIn(
            2,
            (item) => item.update('count', (count) => count + 1)
        )
    )
    

    Example:

    const state = Immutable.fromJS({
        productInfo: {
            price: '€10.99'
        }
    })
    
    const state2 = state.update(
        ['productInfo', 'price'],
        (priceString) => priceString.replace('€', '$')
    )
    )
    // state2 = Immutable.Map({
    //     productInfo: {
    //         price: '$10.99'
    //     }
    // })
    

    Merge Operations #

    In Redux reducers in Progressive Web, we often receive large, complex JSON objects from the backend, which need to be included in the state. For that, we have merge methods, which take one or more objects to be merged into the Immutable object.

    Note that these will only work correctly if the Immutable state object is deeply immutable. This means that there are no plain Javascript objects or arrays anywhere in the tree. If the state is not deeply immutable, the result of the merge is difficult to predict. If we use Immutable.fromJS to build the initial states of our reducers, they are guaranteed to be deeply immutable, but we must still be careful not to set plain JS objects into the tree with the .set and .update methods.

    These merge operations can take both Immutable and plain JS objects as arguments. Any JS objects that are added to the final Immutable object are converted deeply to Immutable objects.

    .mergeDeep #

    See also: Map, List

    The most basic of the merge methods we use is .mergeDeep. It merges its arguments into the container, with keys from later arguments overriding those from earlier arguments (similar to Object.assign or object spread operators). If the value of a key is itself a composite object, it will recursively merge those in creating the final object.

    It takes an arbitrary number of arguments, which should all be either Immutable objects of the same type as the original object, or the corresponding plain Javascript object type.

    One possible problem with using .mergeDeep is that, in merging the contents of lists, it will not shorten the list to match the new content. If we would like lists to be overwritten, we can use the .mergeWith function, as described below.

    Examples:

    const state = Immutable.fromJS({
        a: 'b',
        test: false,
        descriptionLink: null,
        data: {
            immutable: false,
            react: true,
            angular: false
        }
    })
    
    const state2 = state1.mergeDeep({
        a: 'c',
        z: 'x',
        descriptionLink: {
            title: 'description',
            href: '/description'
        },
        data: {
            immutable: true,
            redux: true
        }
    })
    
    // state2 is equal to:
    
    const state3 = Immutable.fromJS({
        a: 'c',
        z: 'x',
        test: false,
        descriptionLink: {
            title: 'description',
            href: '/description'
        }
        data: {
            immutable: true,
            react: true,
            redux: true,
            angular: false
        }
    })
    

    .mergeDeepIn #

    See also: Map, List

    The .mergeDeepIn method is the same as mergeDeep but starts at a given path rather than the root. It takes any number of arguments, the first of which needs to be an array or Immutable List containing a path.

    Equivalent:

    state.mergeDeepIn(['inner', 'key'], {item: 'test thing'})
    
    // is equivalent to
    
    state.mergeDeep({inner: {key: {item: 'test thing'}}})
    

    Example:

    const state = Immutable.fromJS({
        a: 'b',
        data: {
            types: {
                name: 'string',
                quantity: 'integer'
            }
        }
    })
    
    const state2 = state1.mergeDeepIn(
        ['data', 'types'],
        {
            quantity: 'number',
            link: {
                href: 'string'
            }
        }
    )
    
    // state2 is equal to:
    
    const state3 = Immutable.fromJS({
        a: 'b',
        data: {
            types: {
                name: 'string',
                quantity: 'number',
                link: {
                    href: 'string'
                }
            }
        }
    })
    

    .mergeWith #

    See also: Map, List

    The .mergeWith function works similar to .mergeDeep, but uses a custom function (passed as the first argument) to merge the parts of the state. We will need to use this when the usual merge algorithm does not provide the results we are looking for.

    One example of this is when we would like to ensure that the contents of lists are truncated to match the new lists, rather than being merged item-by-item. We encounter this in a shopping cart, for instance, when we delete an item. The new list contains fewer elements, and those elements are merged into the pre-existing elements, but the additional element is left in the list by .mergeDeep.

    We have a helper function mergeSkipLists that is useful with .mergeWith for this purpose, used in the example below.

    Example:

    const state = Immutable.fromJS({
        items: [
            {name: 'a'},
            {name: 'b'},
            {name: 'c'}
        ]
    })
    
    const mergeObject = {
        items: [
            {name: 'd'}
        ]
    }
    
    const state2 = state.mergeDeep(mergeObject)
    
    const state3 = state.mergeWith(mergeSkipLists, mergeObject)
    
    // state2 is equal to:
    
    const state2 = Immutable.fromJS({
        items: [
            {name: 'd'},
            {name: 'b'},
            {name: 'c'}
        ]
    })
    
    // state3 is equal to:
    
    const state3 = Immutable.fromJS({
        items: [
            {name: 'd'}
        ]
    })