Search code examples
javascriptreactjsreduximmutable.js

How to update or add to immutable.js List


I'm using Redux and Immutable.js in my React-based project (built on React Boilerplate) and I'm looking for an idiomatic way to update or add to an Immutable.js List.

My current setup. State initially looks like this:

const initialState = fromJS({
  accounts: [],
  activeAccount: null,
  loadedAccounts: [],
});

I have an Immutable Record for an account object:

const account = new Record({
  description: '',
  id: '',
  name: '',
  status: '',
});

And my reducer:

function reducer(state = initialState, action) {
  switch (action.type) {
    case LOAD_ACCOUNT_SUCCESS:
      return state
        .set('activeAccount', new account(action.account))
    default:
      return state;
  }
}

This works fine - when a LOAD_ACCOUNT_SUCCESS action is fired, activeAccount is updated to the value of action.account.

I can amend this so that every new LOAD_ACCOUNT_SUCCESS action pushes the newly-loaded account data to loadedAccounts instead:

function reducer(state = initialState, action) {
  switch (action.type) {
    case LOAD_ACCOUNT_SUCCESS:
      const loadedAccount = new account(action.account);
        return state
          .update('loadedAccounts', (loadedAccounts) => loadedAccounts.push(loadedAccount));
    default:
      return state;
  }
}

However, at the moment loading the same account data twice will result in new Records being pushed to my List each time (duplicating data). What I want to do instead is either add action.account to loadedAccounts (as happens now) or update the Record in the List if there is a matching ID. I'm looking at similar questions and the lamentable Immutable.js documentation and I can't see how to do this: no syntax I've tried works as I expect here.


Solution

  • So, what do you need here is nested update. At first, you have to check your list of loadedAccounts whether it has this account or not. Secondly, you have to change activeAccount field. And, lastly, add (or update) account to loadedAccounts.

    The caveat here is how you pass account property. If you derive it from somewhere and pass around as a Record, you can just compare by === (or by .equals()), but it seems that it is just a plain javascript object – I'll suppose it later.

    In terms of code it would be something like:

    // we can do it by different ways, it is just one of them
    const listWithLoadedAccounts = state.get('loadedAccounts');
    const isAccountAlready = Boolean(
      listWithLoadedAccounts.filter(
        account => account.get('id') === action.account.id
      ).size
    );
    
    const patchedState = state.set('activeAccount', action.account.id);
    return isAccountAlready
      ? patchedState.updateIn(['loadedAccounts'], list => list.map(account => account.get('id') === account.action.id ? new account(action.account) : account))
      : patchedState.updateIn(['loadedAccounts'], list => list.concat(new account(action.account)))
    

    It is not the ideal code, something can be deduplicated, but you get the idea – always use deep merge / update if you need to change nested fields or data structures.

    You also can set new field directly, like:

    const oldList = state.get('loadedAccounts');
    const newList = oldList.concat(action.account);
    const patchedState = state.set('loadedAccounts', newList);
    

    But I personally find that it is not that flexible and also not consistent, because it is quite common operation to perform deep merge.