From c9b83b5e08f0d9763914f9894777a3e170b62849 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Mon, 13 Mar 2023 14:54:47 +0200 Subject: [PATCH] Lodash: Refactor away from `_.set()` in core-data (#48784) * Lodash: Refactor away from _.set() in core-data * Add graceful support and more tests --- .../core-data/src/queried-data/selectors.js | 5 +- packages/core-data/src/selectors.ts | 9 ++- packages/core-data/src/utils/index.js | 1 + .../core-data/src/utils/set-nested-value.js | 37 ++++++++++ .../src/utils/test/set-nested-value.js | 72 +++++++++++++++++++ 5 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 packages/core-data/src/utils/set-nested-value.js create mode 100644 packages/core-data/src/utils/test/set-nested-value.js diff --git a/packages/core-data/src/queried-data/selectors.js b/packages/core-data/src/queried-data/selectors.js index 17372f390d3ce5..a3e6f322274ced 100644 --- a/packages/core-data/src/queried-data/selectors.js +++ b/packages/core-data/src/queried-data/selectors.js @@ -3,12 +3,12 @@ */ import createSelector from 'rememo'; import EquivalentKeyMap from 'equivalent-key-map'; -import { set } from 'lodash'; /** * Internal dependencies */ import getQueryParts from './get-query-parts'; +import { setNestedValue } from '../utils'; /** * Cache of state keys to EquivalentKeyMap where the inner map tracks queries @@ -70,7 +70,8 @@ function getQueriedItemsUncached( state, query ) { field.forEach( ( fieldName ) => { value = value[ fieldName ]; } ); - set( filteredItem, field, value ); + + setNestedValue( filteredItem, field, value ); } } else { // If expecting a complete item, validate that completeness, or diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index fd91452d6549b7..07f3c9f48c5ebb 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -2,7 +2,6 @@ * External dependencies */ import createSelector from 'rememo'; -import { set } from 'lodash'; /** * WordPress dependencies @@ -17,7 +16,11 @@ import deprecated from '@wordpress/deprecated'; import { STORE_NAME } from './name'; import { getQueriedItems } from './queried-data'; import { DEFAULT_ENTITY_KEY } from './entities'; -import { getNormalizedCommaSeparable, isRawAttribute } from './utils'; +import { + getNormalizedCommaSeparable, + isRawAttribute, + setNestedValue, +} from './utils'; import type * as ET from './entity-types'; // This is an incomplete, high-level approximation of the State type. @@ -336,7 +339,7 @@ export const getEntityRecord = createSelector( field.forEach( ( fieldName ) => { value = value[ fieldName ]; } ); - set( filteredItem, field, value ); + setNestedValue( filteredItem, field, value ); } return filteredItem as EntityRecord; } diff --git a/packages/core-data/src/utils/index.js b/packages/core-data/src/utils/index.js index c17633c6a17d1d..4f4149c8265b2b 100644 --- a/packages/core-data/src/utils/index.js +++ b/packages/core-data/src/utils/index.js @@ -6,3 +6,4 @@ export { default as onSubKey } from './on-sub-key'; export { default as replaceAction } from './replace-action'; export { default as withWeakMapCache } from './with-weak-map-cache'; export { default as isRawAttribute } from './is-raw-attribute'; +export { default as setNestedValue } from './set-nested-value'; diff --git a/packages/core-data/src/utils/set-nested-value.js b/packages/core-data/src/utils/set-nested-value.js new file mode 100644 index 00000000000000..cb7db8a04b4b07 --- /dev/null +++ b/packages/core-data/src/utils/set-nested-value.js @@ -0,0 +1,37 @@ +/** + * Sets the value at path of object. + * If a portion of path doesn’t exist, it’s created. + * Arrays are created for missing index properties while objects are created + * for all other missing properties. + * + * This function intentionally mutates the input object. + * + * Inspired by _.set(). + * + * @see https://lodash.com/docs/4.17.15#set + * + * @param {Object} object Object to modify + * @param {Array} path Path of the property to set. + * @param {*} value Value to set. + */ +export default function setNestedValue( object, path, value ) { + if ( ! object || typeof object !== 'object' ) { + return object; + } + + path.reduce( ( acc, key, idx ) => { + if ( acc[ key ] === undefined ) { + if ( Number.isInteger( path[ idx + 1 ] ) ) { + acc[ key ] = []; + } else { + acc[ key ] = {}; + } + } + if ( idx === path.length - 1 ) { + acc[ key ] = value; + } + return acc[ key ]; + }, object ); + + return object; +} diff --git a/packages/core-data/src/utils/test/set-nested-value.js b/packages/core-data/src/utils/test/set-nested-value.js new file mode 100644 index 00000000000000..bbc71291807644 --- /dev/null +++ b/packages/core-data/src/utils/test/set-nested-value.js @@ -0,0 +1,72 @@ +/** + * Internal dependencies + */ +import setNestedValue from '../set-nested-value'; + +describe( 'setNestedValue', () => { + it( 'should return the same object unmodified if path is an empty array', () => { + const input = { x: 'y' }; + const result = setNestedValue( input, [], 123 ); + + expect( result ).toBe( input ); + expect( result ).toEqual( { x: 'y' } ); + } ); + + it( 'should set values at deep level', () => { + const input = { x: { y: { z: 123 } } }; + const result = setNestedValue( input, [ 'x', 'y', 'z' ], 456 ); + + expect( result ).toEqual( { x: { y: { z: 456 } } } ); + } ); + + it( 'should create nested objects if necessary', () => { + const result = setNestedValue( {}, [ 'x', 'y', 'z' ], 123 ); + + expect( result ).toEqual( { x: { y: { z: 123 } } } ); + } ); + + it( 'should create nested arrays when keys are numeric', () => { + const result = setNestedValue( {}, [ 'x', 0, 'z' ], 123 ); + + expect( result ).toEqual( { x: [ { z: 123 } ] } ); + } ); + + it( 'should also work with arrays', () => { + const result = setNestedValue( [], [ 0, 1, 2 ], 123 ); + + expect( result ).toEqual( [ [ , [ , , 123 ] ] ] ); + } ); + + it( 'should keep remaining properties unaffected', () => { + const input = { x: { y: { z: 123, z1: 'z1' }, y1: 'y1' }, x1: 'x1' }; + const result = setNestedValue( input, [ 'x', 'y', 'z' ], 456 ); + + expect( result ).toEqual( { + x: { y: { z: 456, z1: 'z1' }, y1: 'y1' }, + x1: 'x1', + } ); + } ); + + it( 'should intentionally mutate the original object', () => { + const input = { x: 'y' }; + const result = setNestedValue( input, [ 'x' ], 'z' ); + + expect( result ).toBe( input ); + expect( result ).toEqual( { x: 'z' } ); + } ); + + it.each( [ + undefined, + null, + 0, + 5, + NaN, + Infinity, + 'test', + false, + true, + Symbol( 'foo' ), + ] )( 'should return the original input if it is %s', ( value ) => { + expect( setNestedValue( value, [ 'x' ], 123 ) ).toBe( value ); + } ); +} );