10785

Redux, normalised entities and lodash merge

I'm using Redux, React and Lodash with a fairly standard normalized entities store.

When I merge in new entities in a redux reducer, the references to all my existing entities change (despite not being modified), causing any pure components to re-render.

Is there an alternative to lodash's merge that can merge whilst maintaining the existing references to values that are not in the object being merged in?

let entities = { [1]: {a: true }, [2]: {a: true, b: true }, } let response = { [2]: {a: false } } let newEntities = _.merge({}, entities, response) console.log(entities[1] === newEntities[1]) // false

I can't use Object.assign/ES6 Spread here as then newEntities[2].b will be deleted.

I do realise there are alternative solutions such as custom sCU and reselect, however it would be much cleaner to take care of this at the reducer level rather than having to modify every single component that does an equality reference check on its props.

Answer1:

Use mergeWith with a customizer:

let keepRef = (objValue, srcValue) => ( objValue === undefined ? srcValue : _.mergeWith({}, objValue, srcValue, keepRef) ) let newEntities = _.mergeWith({}, entities, response, keepRef)

Answer2:

I expanded on @Pavlo's awesome answer. I added support for arrays, and collections. I define a collection as an array of object's, where each object has an id key. This is very common in react/redux and normalized data.

import { mergeWith, isPlainObject, isEmpty, keyBy } from 'lodash' // https://stackoverflow.com/a/49437903/1828637 // mergeWith customizer. // by default mergeWith keeps refs to everything, // this customizer makes it so that ref is only kept if unchanged // and a shallow copy is made if changed. this shallow copy continues deeply. // supports arrays of collections (by id). function keepUnchangedRefsOnly(objValue, srcValue) { if (objValue === undefined) { // do i need this? return srcValue; } else if (srcValue === undefined) { // do i need this? return objValue; } else if (isPlainObject(objValue)) { return mergeWith({}, objValue, srcValue, keepUnchangedRefsOnly); } else if (Array.isArray(objValue)) { if (isEmpty(objValue) && !isEmpty(srcValue))return [...srcValue]; else if (!isEmpty(objValue) && isEmpty(srcValue)) return objValue; else if (isEmpty(objValue) && isEmpty(srcValue)) return objValue; // both empty else { // if array is array of objects, then assume each object has id, and merge based on id // so create new array, based objValue. id should match in each spot if (isPlainObject(objValue[0]) && objValue[0].hasOwnProperty('id')) { const srcCollection = keyBy(srcValue, 'id'); const aligned = objValue.map(el => { const { id } = el; if (srcCollection.hasOwnProperty(id)) { const srcEl = srcCollection[id]; delete srcCollection[id]; return mergeWith({}, el, srcEl, keepUnchangedRefsOnly); } else { return el; } }); aligned.push(...Object.values(srcCollection)); return aligned; } else { return [ ...objValue, ...srcValue ]; } } } }

Usage:

const state = { chars: ['a', 'b'], messages: [ { id: 1, text: 'one' }, { id: 2, text: 'ref to this entry will be unchanged' } ] } const response = { chars: ['c', 'd'], messages: [ { id: 1, text: 'changed ref text one' }, { id: 3, text: 'three' } ] } const stateNext = mergeWith({}, state, response, keepUnchangedRefsOnly)

Resulting stateNext is:

{ chars: [ 'a', 'b', 'c', 'd' ], messages: [ { id: 1, text: 'changed ref text one' }, { 'id': 2, text: 'ref to this entry will be unchanged' }, { 'id': 3, text: 'three' } ] }

If you want to keep undefined values, then replace mergeWith in customizer and your use case with assignWith. Example - https://stackoverflow.com/a/49455981/1828637

Recommend

  • PHP imagettftext return bounding box differs from rendered bounding box
  • Stdout captured from pipe in Python is truncated
  • Can I use urlfetch with self signed certifications over ssl
  • ES6 import vs in html [duplicate]
  • Javascript function object body
  • Appium Error: Could not get Xcode version
  • Grouping vars in function
  • Changing the user agent in Chromium Embedded 3 (DCEF3) (CefVCL)
  • Objective-C – access extern const with a string containing its name? [duplicate]
  • WP7 - read from CSV file? Or what to do with the data?
  • How do I properly code a javascript property and method using the 'prototype' function?
  • How can I merge my files when the folder structure has changed using Borland StarTeam?
  • Crafting a LINQ based solution to determine if a set of predicates are satisfied for a pair of colle
  • Rails redirect_to another controller method throwing “Template missing”
  • Get UILabel out of UIButton
  • How to use the resource module to measure the running time of a function?
  • Using an STL Iterator without initialising it
  • Mocha throws unexpected token error for ES6 object spread operator
  • SSL client cert authentication for only some URLs?
  • Criterion causing memory consumption to explode, no CAFs in sight
  • C++ Single function pointer for all template instances
  • How to copy styled text in JTextPane
  • Set focus to first invalid form element in AngularJS
  • Find group of records that match multiple values
  • How to autopopulate a field in SugarCRM form
  • Clear activity stack before launching another activity
  • Assign variable to the value in HTML
  • OOP Javascript - Is “get property” method necessary?
  • C++ Partial template specialization - design simplification
  • NHibernate Validation Localization with S#arp Architecture
  • Control modification in presentation layer
  • Sails.js/waterline: Executing waterline queries in toJSON function of a model?
  • Master page gives error
  • What is Eclipse's Declaration View used for?
  • Bug in WPF DataGrid
  • Can Jackson SerializationFeature be overridden per field or class?
  • Do create extension work in single-user mode in postgres?
  • Android Studio and gradle
  • how does django model after text[] in postgresql [duplicate]
  • How to get NHibernate ISession to cache entity not retrieved by primary key