ReactJS: How to access and update nested state object with dynamic key? - javascript

Suppose I have a component with state defined as follows:
this.state = {
apple:{
a:1,
b:2,
},
mango:{
banana : {
a:1,
b:2,
}
}
}
If I wanted to update the value of a nested object in my state, I could do so with hard coded keys as shown below:
cost temp = { ...this.state['mango'] }
temp['banana']['a'] = 2;
this.setState({mango:temp});
How would I update a nested value in my state object dynamically key? For example, if I had a JSON path in either dot or array notation, how could I update my component state?

One way to achieve this would be to acquire the nested object that is the parent of the field that your path is targeting via Array#reduce:
const nestedObject = path
.slice(0, -1)
.reduce((object, part) => (object === undefined ? undefined : object[part]), { ...state })
And then update the last key/value of nestedObject by via the last key of your path:
/* Get last part of path, and update nestedObject's value for this key, to 2 */
const [pathTail] = path.slice(-1);
nestedObject[pathTail] = 2;
The following snippet shows these two ideas together:
/* Path of nested field to update, in array notation */
const path = ['mango', 'banana', 'a'];
/* Components state */
const state = {
apple: {
a: 1,
b: 2,
},
mango: {
banana: {
a: 1,
b: 2,
}
}
};
const stateClone = { ...state };
/* Aquire the parent object (ie banana) of the target field (ie a) */
const nestedObject = path
.slice(0, -1)
.reduce((object, part) => (object === undefined ? undefined : object[part]), stateClone)
if (nestedObject !== undefined) {
/* Obtain last key in path */
const [pathTail] = path.slice(-1);
/* Update value of last key on target object to new value */
nestedObject[pathTail] = 2;
}
/* Display updated state */
console.log('Updated state:', stateClone)
/* Call this.setState: */
// this.setState(stateClone);
Update
Here is some extra detail outlining how the reduce() part of the answer works:
path
/* slice obtains ['mango', 'banana'], seeing -1 clips last item */
.slice(0, -1)
/* reduce iterates through each part of array ['mango', 'banana']
where at each iteration we fetch the corresponding nested object
of the { ...state } object that's passed in */
.reduce((object, part) => {
/* At iteration 1:
object has two keys, 'apple' and 'mango'
part is 'mango'
object is defined, so return object['mango'] for first iteration
At iteration 2:
object passed from last iteration has one key, 'banana'
part is 'banana'
object is defined, so return object['banana'] for second iteration
Reduce complete:
we return object['banana'], which is the same as state['mango']['banana']
*/
if(object === undefined) { return undefined; }
return object[part]
}, stateClone)

Having:
const [formState, setFormState] = useState(
{
id:1,
name:'Name',
innerObjectName: {
propA: 'Something',
propB: 'Another thing',
}
});
Maybe you're looking for something like this:
const handleComplexInputChange = (evt, object) => {
setFormState({
...formState,
[object] : {
...formState[object],
[evt.target.name]: evt.target.value,
}
})
}
And from your component you should call it like this:
onChange={(e) => {
handleComplexInputChange(e, "innerObjectName");
}}

Related

How to check if an object has other than specific properties

I have an object obj which has n number of possible properties
lets say some of them are known,
const someKnownProps = ["props.abc", "xyz"]; // or more
I want to know if obj has other than known properties in it.
To clarify:
obj can look like this:
obj = {
props: {
abc: {
def: 1
},
ghi: {
jkl: 2
}
},
xyz: 3
}
Doing Object.keys only return first level children,
in this case it will return props not props.abc
You can use Object.keys to get all keys and filter the keys which aren't included in the someKnownProps array.
const obj = {
"props.abc": 1,
"xyz": 2,
"three": 3,
"four": 4,
}
const someKnownProps = ["props.abc", "xyz"]; // or more
const unknownKeys = Object.keys(obj).filter(key => !someKnownProps.includes(key))
console.log(unknownKeys)
There are two (unrelated) tasks involved in this question:
Traversal of an object's properties
Comparison of a set of traversed object properties to a list of strings representing dot-notation-formatted object property accessors
While I'm sure the former has been previously discussed on SO, I'll provide an implementation of such an algorithm below in order to address the details of this question.
This is essentially a specific case of recursion where each cycle starts with these inputs:
an object
a dot-notation-formatted path
a Set of existing such paths
The code below includes inline comments explaining what's happening, and there are some console.log statements at the end to help you visualize some example results based on the data in your question. If something is unclear after reviewing the code, feel free to leave a comment.
'use strict';
/** #returns whether value is a non-null, non-array object */
function isObject (value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
/** #returns the enumerable (optionally including inherited) keys of an object */
function getKeys (obj, includeInherited = false) {
if (!includeInherited) return Object.keys(obj);
const keys = new Set();
let o = obj;
while (o !== null) {
for (const key of Object.keys(o)) keys.add(key);
o = Object.getPrototypeOf(o);
}
return [...keys];
}
/**
* #returns an array of strings representing all traversible branches
* of child objects, each formatted as a combined path of dot-notation
* property accessors
*/
function findObjectPaths (
obj,
{
includeInherited = false,
currentPath = '',
paths = new Set(),
skipReturn = false,
} = {},
) {
for (const key of getKeys(obj, includeInherited)) {
// Append the current dot-notation property accessor
// to the existing path of this object:
const path = `${currentPath}.${key}`;
// Add it to the set:
paths.add(path);
const o = obj[key];
// Recurse if the child value is an object:
if (isObject(o)) {
findObjectPaths(o, {
includeInherited,
currentPath: path,
paths,
skipReturn: true,
});
}
}
// If this is not a sub-cycle (it's the top-level invocation), then convert
// the set to an array and remove the first "." from each string
if (!skipReturn) return [...paths].map(p => p.slice(1));
}
// Use:
const obj = {
props: {
abc: {
def: 1,
},
ghi: {
jkl: 2,
},
},
xyz: 3,
};
let someKnownProps = ['props.abc', 'xyz'];
let objectPaths = findObjectPaths(obj);
let hasOtherProps = objectPaths.some(path => !someKnownProps.includes(path));
console.log(hasOtherProps); // true
// An example of all of the paths in the object above:
someKnownProps = [
'props',
'props.abc',
'props.abc.def',
'props.ghi',
'props.ghi.jkl',
'xyz',
];
objectPaths = findObjectPaths(obj);
hasOtherProps = objectPaths.some(path => !someKnownProps.includes(path));
console.log(hasOtherProps); // false
// Finally, comparing the results of inherited vs non-inherited enumeration:
const objWithoutOwnProps = Object.create({
props: {
abc: {
def: 1,
},
ghi: {
jkl: 2,
},
},
xyz: 3,
});
console.log(
'Non-inherited props:',
findObjectPaths(objWithoutOwnProps),
);
console.log(
'Inherited props:',
findObjectPaths(objWithoutOwnProps, {includeInherited: true}),
);
Similar to what Mina said:
let obj = {one: 1, two: 2, three: 3};
let knownKeys = ['one', 'two'];
for (let key in obj) {
if (!knownKeys.includes(key)) {
console.log(key);
}
}

How to make sure Array has object items only with unique keys

I'm creating an online store. The product has attributes (in this case, iMac computer has attributes like: capacity, usb features and digital keyboard.)
I store the attributes in my state.
The problem is, every time I switch from, for example, 256GB capacity to 512GB capacity, it adds an entire new object with {capacity:"512GB"} to the array.
How do I configure my handler function (code below) so that it conditionally checks if the object in an array already has 'Capacity' key, and updates that to a new selection, instead of adding another object? I tried everything, I'm desperate
the handler receives object with key-value pair, and the key (as label) itself, and type. In this case, type can be ignored.
const handleAttributeChange = (object, type, label) => {
if (type == "text") {
this.setState({
selectedAttributes: {
id: id,
text: [...this.state.selectedAttributes.text, object],
swatch: this.state.selectedAttributes.swatch,
},
});
} else if ((type = "swatch")) {
this.setState({
selectedAttributes: {
id: id,
text: this.state.selectedAttributes.text,
swatch: [...this.state.selectedAttributes.swatch, object],
},
});
}
};
Instead of an array, you can take objects with diff key values. If keys are unique. Later u can convert it into the list of values.
let obj = {};
const update = (o) => {
obj = { ...obj, ...o };
};
/* In case you know key, value */
const update2 = (key, value) => {
obj[key] = value;
};
console.log(obj);
update({ a: 1 });
update({ b: 2 });
console.log(obj); // { a: 1, b: 2 }
update({ a: 3 });
console.log(obj); // { a: 3, b: 2 }
console.log(Object.entries(obj).map(([key, value]) => ({ [key]: value })));
//[ { a: 3 }, { b: 2 } ]

Dynamically setState() with key nested state

I have a method in a component. I want to dynamically setState with a key in a nested array of objects.
method = (name, value) => {
console.log(name)
//a //value is 1
//b //value is 2
//c //value is 3
this.setState({ [name]:value })
}
when its not nested, it dynamically changes state successfully. However when its nested
method = (name, value) => {
this.setState({
ArrayOfObjects:[{
[name] : value
}]
}
My state becomes
state = {
ArrayOfObjects: [{
c: 3
}]
}
I want
state = {
ArrayOfObjects: [{
a: 1,
b: 2,
c: 3
}]
What's wrong?
You could just push an element to the current ArrayOfObjects.
ArrayOfObjects = this.state.ArrayOfObjects;
ArrayOfObjects.push({[name] : value});
this.setState({
ArrayOfObjects
});
Or using the spread operator:
this.setState({
ArrayOfObjects: [
...this.state.ArrayOfObjects,
{[name] : value}
]
});
Assuming that ArrayOfObjects is an always an array with single object and that you want to merge name/value into that object:
method(name, value) {
// make a copy
const ArrayOfObjects = [...this.state.ArrayOfObjects];
// merge properties and set dynamic value
ArrayOfObjects[0] = { ...ArrayOfObjects[0], [name]: value };
this.setState({
ArrayOfObjects
});
}
Here is an example in action.
Hopefully that helps!

Nest/Group an array of objects by attribute, ignoring null/undefined and using additional property as label

I'm trying to nest-group an array of objects.
The function provided by this gist almost works as intended and uses lodash as basis:
https://gist.github.com/joyrexus/9837596
const _ = require('lodash');
function nest(seq, keys) {
if (!keys.length) return seq;
let [first, ...rest] = keys;
return _.mapValues(_.groupBy(seq, first), value => nest(value, rest));
}
This recursively,
However, there are two problems I face.
if a parameter is set to null or undefined, it is used as a group, instead the
an optional object attribute should be used as the final object key, so there are only objects, no arrays. This attribute always has to be unique in order to work correctly.
Is it possible to combine or extend the existing nest function to solve the above points?
The pros of this method is, that instead of the keys, I can also use an array of functions (p => p.parameterGroup1) to return the parameter. So instead of a last optional parameter, I could also use p => p.parameterGroup1 ? p.parameterGroup1 : p.label
I prepared a little test, to give you a better idea of what is expected:
test('nest array of objects by groups as keys, stopping at null and using a final label param', t => {
let properties = [
{
parameterGroup1: 'first',
parameterGroup2: 'second',
parameterGroup3: 'third',
label: 'A'
},
{
parameterGroup1: 'first',
parameterGroup2: 'second',
parameterGroup3: null,
label: 'B'
},
{
parameterGroup1: 'a',
parameterGroup2: 'b',
parameterGroup3: undefined,
label: 'C'
},
]
let expected = {
first: {
second: {
third: {
A: {
parameterGroup1: 'first',
parameterGroup2: 'second',
parameterGroup3: 'third',
label: 'A'
}
},
B: {
parameterGroup1: 'first',
parameterGroup2: 'second',
parameterGroup3: null,
label: 'B'
}
}
},
a: {
b: {
C: {
parameterGroup1: 'a',
parameterGroup2: 'b',
parameterGroup3: undefined,
label: 'C'
}
}
}
}
let grouped = nest(properties, ['parameterGroup1', 'parameterGroup2', 'parameterGroup3'], 'label')
t.deepEqual(grouped, expected)
})
Thank you in advance!
Here is a way to do it in vanilla js. We construct the result object by reduceing the array seq: For each object obj in the array seq, we walk the result object level by level using the values from obj of the keys from keys. If the value is null or undefined, we skip (won't go down another level). If the value exist we go down a level, creating a level (object) if it doen't already exist. We do that repeatedly using a reduce on the keys array untill we find the leaf object (last level), to which we assign our current object under the key obtained evaluating obj[last]:
function nest(seq, keys, last) {
return seq.reduce((result, obj) => {
// First we find the (last level) object to which we will assign our current object to, as a child
let lastLevel = keys.reduce((res, key) => { // for each key in keys
let value = obj[key]; // get the value from our current object obj for that key key
if(value == null) return res; // if the value is null or undefined, skip
if(res[value]) return res[value]; // if the level for value exists return it
return res[value] = {}; // if it doesn't, create a new level, assing it to result and return it
}, result);
// then we assign it using the value of the key last
lastLevel[obj[last]] = obj; // we found the last possible level, assign obj to it under the key obj[last]
return result;
}, {});
}
Example:
function nest(seq, keys, last) {
return seq.reduce((result, obj) => {
let lastLevel = keys.reduce((res, key) => {
let value = obj[key];
if(!value) return res;
if(res[value]) return res[value];
return res[value] = {};
}, result);
lastLevel[obj[last]] = obj;
return result;
}, {});
}
let properties = [{parameterGroup1: 'first',parameterGroup2: 'second',parameterGroup3: 'third',label: 'A'},{parameterGroup1: 'first',parameterGroup2: 'second',parameterGroup3: null,label: 'B'},{parameterGroup1: 'a',parameterGroup2: 'b',parameterGroup3: undefined,label: 'C'}];
let grouped = nest(properties, ['parameterGroup1', 'parameterGroup2', 'parameterGroup3'], 'label');
console.log(grouped);

_.assign only if property exists in target object

My need is to do something like an _.assign, but only if the target object already has the property being assigned. Think of it like the source objects may have some properties to contribute, but also some properties that I don't want to mix in.
I haven't ever used _.assign's callback mechanism, but tried the following. It 'worked', but it still assigned the property to the dest object (as undefined). I don't want it to assign at all.
_.assign(options, defaults, initial, function (destVal, sourceVal) {
return typeof destVal == 'undefined' ? undefined : sourceVal;
});
I wrote the following function to do this, but wondering if lodash already has something baked in that is more elegant.
function softMerge (dest, source) {
return Object.keys(dest).reduce(function (dest, key) {
var sourceVal = source[key];
if (!_.isUndefined(sourceVal)) {
dest[key] = sourceVal;
}
return dest;
}, dest);
}
You could take just the keys from the first object
var firstKeys = _.keys(options);
Then take a subset object from the second object, taking only those keys which exist on the first object :
var newDefaults = _.pick(defaults, firstKeys);
Then use that new object as your argument to _.assign :
_.assign(options, newDefaults);
Or in one line :
_.assign(options, _.pick(defaults, _.keys(options)));
Seemed to work when I tested it here : http://jsbin.com/yiyerosabi/1/edit?js,console
Here is a immutable deep version, I call it "merge that retains the shape", in TypeScript that uses lodash:
function _mergeKeepShapeArray(dest: Array<any>, source: Array<any>) {
if (source.length != dest.length) {
return dest;
}
let ret = [];
dest.forEach((v, i) => {
ret[i] = _mergeKeepShape(v, source[i]);
});
return ret;
}
function _mergeKeepShapeObject(dest: Object, source: Object) {
let ret = {};
Object.keys(dest).forEach((key) => {
let sourceValue = source[key];
if (typeof sourceValue !== "undefined") {
ret[key] = _mergeKeepShape(dest[key], sourceValue);
} else {
ret[key] = dest[key];
}
});
return ret;
}
function _mergeKeepShape(dest, source) {
// else if order matters here, because _.isObject is true for arrays also
if (_.isArray(dest)) {
if (!_.isArray(source)) {
return dest;
}
return _mergeKeepShapeArray(dest, source);
} else if (_.isObject(dest)) {
if (!_.isObject(source)) {
return dest;
}
return _mergeKeepShapeObject(dest, source);
} else {
return source;
}
}
/**
* Immutable merge that retains the shape of the `existingValue`
*/
export const mergeKeepShape = <T>(existingValue: T, extendingValue): T => {
return _mergeKeepShape(existingValue, extendingValue);
}
And a simple test to see how I vision such merge should work:
let newObject = mergeKeepShape(
{
a : 5,
// b is not here
c : 33,
d : {
e : 5,
// f is not here
g : [1,1,1],
h : [2,2,2],
i : [4,4,4],
}
},
{
a : 123,
b : 444,
// c is not here
d : {
e : 321,
f : 432,
// g is not here
h : [3,3,3],
i : [1,2],
}
}
);
expect(newObject).toEqual({
a : 123,
// b is not here
c : 33,
d : {
e : 321,
// f is not here,
g : [1,1,1],
h : [3,3,3],
i : [4,4,4]
}
});
I used seamless-immutable myself in the test, but didn't see a need to put it in this answer.
I hereby place this in the Public Domain.
Another way to accomplish this is by combining _.mapObject with _.has
_.mapObject(object1, function(v, k) {
return _.has(object2, k) ? object2[k] : v;
});
Explanation:
Traverse all key/value pairs of object1 using _.mapObject
Using _.has, check if property name k also exists in object2.
If it does, copy the value assigned to key object2's k back to object1, else, just return the existing value of object1 (v).
Plunkr
Following #svarog's answer I came up with this (lodash version 4.17.15):
const mergeExistingProps = (target, source) => _.mapValues(target, (value, prop) => _.get(source, prop, value));
I recently have the same need in my personal project, I need to fill the value from one object(SOURCE) to another object(TARGET) but don't expand its property. Also, some additional requirements should be met:
Any property with a null value in the source will not update to the target;
Any value from the source can be updated into target if such property in target has null value.
The property that holds an array in the target will be loaded based on data from the source, but all entries of the array will remain the same as the target array (so an empty array in the target will not get any data since the item has no property)
Property of the target holding a 2-d array (array has another array as its item) will not be updated, since the meaning of merging two 2-d arrays with a different shape is not clear to me.
Below is an example (Detailed explained in the code):
Assume you have a resume object holding all the data about you, you want to fill the data into the company's application form (also an object). You want the result to have the identical shape of the application form since the company doesn't care about other things, then you can think your resume is SOURCE and the application form is TARGET.
Note that the "additional" field in TARGET is null, which means anything can be updated here based on SOURCE data (As rule #2)
The console output is in JSON format, copy it to some JSON to JS-OBJ converter such as
https://www.convertsimple.com/convert-json-to-javascript/
to have a better view
const applicationForm = {
name: 'Your Name',
gender: 'Your Gender',
email: 'your#email.com',
birth: 0,
experience: [ // employer want you list all your experience
{
company: 'Some Company',
salary: 0,
city: ['', '', ''], // list all city worked for each company
}
],
language: { // employer only care about 2 language skills
english: {
read: false,
write: false,
speak: 'Speak Level'
},
chinese: {
read: false,
write: false,
speak: 'Speak Level'
}
},
additional: null // add anything you want the employer to know
}
const resume = {
name: 'Yunfan',
gender: 'Male',
birth: 1995,
phone: '1234567',
email: 'example#gmail.com',
experience: [
{
company: 'Company A',
salary: 100,
city: ['New York', 'Chicago', 'Beijing'],
id: '0001',
department: 'R&D'
},
{
company: 'Company B',
salary: 200,
city: ['New York'],
id: '0002',
department: 'HR'
},
{
company: 'Company C',
salary: 300,
city: ['Tokyo'],
id: '0003',
}
],
language: {
english: {
read: true,
write: true,
speak: 'Native Speaker'
},
chinese: {
read: true,
write: false,
speak: 'HSK Level 3'
},
spanish: {
read: true,
write: true,
speak: 'Native Speaker'
}
},
additional: {
music: 'Piano',
hometown: 'China',
interest: ['Cooking', 'Swimming']
}
}
function safeMerge(source, target) {
// traverse the keys in the source object, if key not found in target or with different type, drop it, otherwise:
// 1. Use object merge if the value is an object (Can go deeper inside the object and apply same rule on all its properties)
// 2. Use array merge if value is array (Extend the array item from source, but keep the obj format of target)
// 3. Assign the value in other case (For other type, no need go deeper, assign directly)
for (const key in source) {
let value = source[key]
const targetValueType = typeof target[key]
const sourceValueType = typeof value
// if key not found in target or type not match
if (targetValueType === 'undefined' || targetValueType !== sourceValueType) {
continue // property not found in target or type not match
}
// for both type in object, need additional check
else if (targetValueType === 'object' && sourceValueType === 'object') {
// if value in target is null, assign any value from source to target, ignore format
if (target[key] === null) {
target[key] = source[key]
}
// if value in target is array, merge the item in source to target using the format of target only if source value is array
else if (Array.isArray(target[key]) && Array.isArray(value)) {
target[key] = mergeArray(value, target[key])
}
// if value in target is 'real' object (not null or array)', use object merge to do recurring merge, keep target format
else if (!Array.isArray(target[key])){
if (!Array.isArray(value) && value !== null) {
safeMerge(value, target[key])
}
}
}
// if target value and source value has same type but not object, assign directly
else if (targetValueType === sourceValueType) {
target[key] = value
}
}
}
function mergeArray(sourceArray, targetArray) {
// the rule of array merge need additional declare, assume the target already have values or objects in save format in the property<Array>,
// otherwise will not merge item from source to target since cannot add item property,
// NOTE: the item in target array will be totally overwrite instead of append on the tail, only the format will be keep,
// so the lenth of this property will same as source, below is a example:
// target = [{a: 1, b: 2}, {a: 3, b: 4}] // Must in same format, otherwise the first one will be standard
// source = [{a: 5, b: 6, c: 7}]
// mergeArray(source, target) => [{a: 5, b: 6}] // use format of target, but data from source
// double check both of values are array
if (!Array.isArray(sourceArray) || !Array.isArray(targetArray)) {
return
}
// if target array is empty, don't push data in, since format is empty
if (targetArray.length === 0) {
return
}
let resultArray = [] // array to save the result
let targetFormat = targetArray[0]
let targetArrayType = typeof targetArray[0]
// assign value from source to target, if item in target array is not object
if (targetArrayType !== 'object'){
sourceArray.forEach((value) => {
// assign value directly if the type matched
if (targetArrayType === typeof value) {
resultArray.push(value)
}
})
}
// if the item in target is null, push anything in source to target (accept any format)
else if (targetArray[0] === null) {
sourceArray.forEach((value) => {
resultArray.push(value)
})
}
// if the item in target is array, drop it (the meaning of merge 2-d array to a 2-d array is not clear, so skip the situation)
else if (!Array.isArray(targetArray[0])){
// the item is a 'real' object, do object merge based on format of first item of target array
sourceArray.forEach((value) => {
safeMerge(value, targetFormat) // data in targetFormat keep changing, so need to save a independent copy to the result
resultArray.push(JSON.parse(JSON.stringify(targetFormat)))
})
}
else {
console.log('2-d array will be skipped')
}
// replace the value of target with newly built array (Assign result to target array will not work, must assign outside)
return resultArray
}
safeMerge(resume, applicationForm)
console.log(JSON.stringify(applicationForm))

Categories

Resources