Redux、规范化实体和 lodash 合并

IT技术 javascript reactjs redux lodash
2021-05-07 01:47:01

我正在使用 Redux、React 和 Lodash 以及相当标准的标准化实体存储。

当我在 redux reducer 中合并新实体时,对我所有现有实体的引用会发生变化(尽管没有被修改),导致任何纯组件重新渲染。

有没有替代 lodash 的合并可以合并的同时保持对不在被合并对象中的值的现有引用的替代方法?

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

我不能在这里使用 Object.assign/ES6 Spread,因为它newEntities[2].b会被删除。

我确实意识到有替代解决方案,例如自定义 sCU 和重新选择,但是在 reducer 级别处理这个问题会更干净,而不是修改每个对其 props 进行相等引用检查的组件。

2个回答

使用mergeWith了定制:

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

我扩展了@Pavlo 的精彩答案。我添加了对数组和集合的支持。我将一个集合定义为一个对象数组,其中每个对象都有一个id键。这在 react/redux 和规范化数据中很常见。

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 ];
            }
        }
    }
}

用法:

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)

结果stateNext是:

{
    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'
        }
    ]
}

如果您想保留undefined值,请将mergeWith自定义程序和您的用例替换为assignWith. 示例 - https://stackoverflow.com/a/49455981/1828637