Why is my recursion returning undefined? I'm trying to "decode" nested children data from mongo which is returned as IDs like:
{
"_id": "613fd030f374cb62f8f91557",
"children": [
"613fd035f374cb62f8f9155b",
"613fd136f374cb62f8f91564",
"613fd1a5f374cb62f8f91571",
"613fd20bf374cb62f8f9157c"
],
...more data
}
My goal is to drill down and convert each child ID to the Object the ID represensents and convert their child IDs to objects then keep going until the child === [] (no children). I'm trying to have the initial parent (613fd030f374cb62f8f91557) have access to all multi-level nested children objects.
This is my code:
const get_documents = (documents) => {
// Loop through each document
documents.map((document) => {
if (document.parent === null) {
//convert children ids (_id) to array of objects
let dbData = [];
document.children.map((id) => {
let dbChildren = documents.find((x) => x._id === id);
dbData.push(dbChildren);
});
let formattedData = [];
dbData.map((child) => {
let formattedObject = {
id: child._id,
name: child.name,
depth: 0,
parent: child.parent,
closed: true,
children: child_recursion(child.children),
};
formattedData.push(formattedObject)
});
}
});
};
const child_recursion = (arr) => {
let dbData = [];
arr.map((id) => {
let dbChildren = documents.find((x) => x._id === id);
dbData.push(dbChildren);
});
let formattedData = [];
dbData.map((child) => {
let newChild = [];
if (child.children.length > 1) {
newChild = child_recursion(child.children);
}
let formattedObject = {
id: child._id,
name: child.name,
depth: 0,
parent: child.parent,
closed: true,
children: newChild,
};
formattedData.push(formattedObject);
if (newChild === []) {
return formattedData
}
});
};
What am I doing wrong in my recursion? Thank you for the help!
What is getting you here is mixing mutation with recursion which tends to make things a lot more messy.
What this line is telling me:
children: child_recursion(child.children),
is that you are always expecting child_recursion to return an array of formatted children.
However, in child_recursion you aren't always returning something. Sometimes you are mutating sometimes instead. Personally, I believe that it tends to be easier to wrap my head around not using mutation.
The process, therefore, should go something like this:
given an object
check if that object has children
if it does convert the children using this function
if it does not, stop recursion
return a new object, created from the input object with my children set to the output of the conversion.
In this way we can convert each child into an object with its children converted and so on.
Also it is somewhat strange that you are trying to convert all documents at once. Instead, as you gave in your question, you should focus on the object you are trying to convert and work downwards from there. If it is the case where objects can be both parents and children then you have a graph, not a tree and recursion would have to be handled differently than you are expecting.
We don't really need two functions to do this, just one and in the case where you already have the objects you are searching you can pass that along as well (if you don't just remove documents and get them from the db or some service instead). We can also use what is called an accumulator to set initial values before our recursion and track them as we recur.
const convert_children = (obj, documents) => {
const convert_children_acc = (obj, documents, parent, depth) => {
let partial_format = {
id: obj._id,
name: obj.name,
depth: depth,
parent: parent,
close: true
}
if (obj.children && obj.children.length === 0) {
return {
...partial_format,
children: []
}
} else {
return {
...partial_format,
children: obj.children.map(child => {
child = documents.find(x => child === x._id);
return convert_children_acc(child, documents, obj._id, depth+1)
})
}
}
}
return convert_children_acc(obj, documents, null, 0);
};
https://jsfiddle.net/5gaLw1y7/
Related
If you have an array as part of your state, and that array contains objects, whats an easy way to update the state with a change to one of those objects?
Example, modified from the tutorial on react:
var CommentBox = React.createClass({
getInitialState: function() {
return {data: [
{ id: 1, author: "john", text: "foo" },
{ id: 2, author: "bob", text: "bar" }
]};
},
handleCommentEdit: function(id, text) {
var existingComment = this.state.data.filter({ function(c) { c.id == id; }).first();
var updatedComments = ??; // not sure how to do this
this.setState({data: updatedComments});
}
}
I quite like doing this with Object.assign rather than the immutability helpers.
handleCommentEdit: function(id, text) {
this.setState({
data: this.state.data.map(el => (el.id === id ? Object.assign({}, el, { text }) : el))
});
}
I just think this is much more succinct than splice and doesn't require knowing an index or explicitly handling the not found case.
If you are feeling all ES2018, you can also do this with spread instead of Object.assign
this.setState({
data: this.state.data.map(el => (el.id === id ? {...el, text} : el))
});
While updating state the key part is to treat it as if it is immutable. Any solution would work fine if you can guarantee it.
Here is my solution using immutability-helper:
jsFiddle:
var update = require('immutability-helper');
handleCommentEdit: function(id, text) {
var data = this.state.data;
var commentIndex = data.findIndex(function(c) {
return c.id == id;
});
var updatedComment = update(data[commentIndex], {text: {$set: text}});
var newData = update(data, {
$splice: [[commentIndex, 1, updatedComment]]
});
this.setState({data: newData});
},
Following questions about state arrays may also help:
Correct modification of state arrays in ReactJS
what is the preferred way to mutate a React state?
I'm trying to explain better how to do this AND what's going on.
First, find the index of the element you're replacing in the state array.
Second, update the element at that index
Third, call setState with the new collection
import update from 'immutability-helper';
// this.state = { employees: [{id: 1, name: 'Obama'}, {id: 2, name: 'Trump'}] }
updateEmployee(employee) {
const index = this.state.employees.findIndex((emp) => emp.id === employee.id);
const updatedEmployees = update(this.state.employees, {$splice: [[index, 1, employee]]}); // array.splice(start, deleteCount, item1)
this.setState({employees: updatedEmployees});
}
Edit: there's a much better way to do this w/o a 3rd party library
const index = this.state.employees.findIndex(emp => emp.id === employee.id);
employees = [...this.state.employees]; // important to create a copy, otherwise you'll modify state outside of setState call
employees[index] = employee;
this.setState({employees});
You can do this with multiple way, I am going to show you that I mostly used. When I am working with arrays in react usually I pass a custom attribute with current index value, in the example below I have passed data-index attribute, data- is html 5 convention.
Ex:
//handleChange method.
handleChange(e){
const {name, value} = e,
index = e.target.getAttribute('data-index'), //custom attribute value
updatedObj = Object.assign({}, this.state.arr[i],{[name]: value});
//update state value.
this.setState({
arr: [
...this.state.arr.slice(0, index),
updatedObj,
...this.state.arr.slice(index + 1)
]
})
}
Consider the following object, which is composed by an unknown number of deeply nested children.
const state = {
id: 1,
children: [
{
id: 3,
children: [
{
id: 4,
children: []
}
]
},
{
id: 2,
children: []
}
]
}
How can I programatically push a new object to the children array of a node knowing only its id and the array of ids of its parents?
I thought using recursion but I couldn't find a solution that worked. I am also using immutability-helper, so I have tried using Array.reduce() to return an object that looked like this:
const newState = {
children: {
[idxOfNodeToChange]: {
children: {$push: newChildren}
}
}
}
so I could pass it to update() but there I am even more stuck since I would still have to traverse through the accumulator every time to go as deep as needed, and I'm not sure how to do that. Any ideas?
Extra info: I'm using a D3 library for React called VX and this structure is required to build a tree component, so I'm stuck on how to add new nodes programatically.
Here you go. You can use this recursive function to search by id and append data to the found node's children array.
function appendChildToNode(node, nodeId, data) {
// If the node is empty, just return it as is.
if (!node) {
return node;
}
let children;
if (node.id === nodeId) {
// If the node has the id we're searching for,
// append the data to its children.
children = [...node.children, data];
} else {
// Otherwise, apply the function recursively to each of its children
children = node.children.map(childNode => appendChildToNode(childNode, nodeId, data));
}
return { ...node, children };
}
It is immutable and you may use it like this:
const newState1 = appendChildToNode(state, 4, { id: 5, children: [] });
const newState2 = appendChildToNode(state, 2, { id: 5, children: [] });
See it working in the example snippet below.
const state = {
id: 1,
children: [{
id: 3,
children: [{
id: 4,
children: []
}]
},
{
id: 2,
children: []
}
]
};
const newState1 = appendChildToNode(state, 4, {
id: 5,
children: []
});
const newState2 = appendChildToNode(state, 2, {
id: 5,
children: []
});
console.log(state); // Orginal state should not be mutated.
console.log(newState1);
console.log(newState2);
function appendChildToNode(node, nodeId, data) {
// If the node is empty, just return it as is.
if (!node) {
return node;
}
let children;
if (node.id === nodeId) {
// If the node has the id we're searching for,
// append the data to its children.
children = [...node.children, data];
} else {
// Otherwise, apply the function recursively to each of its children
children = node.children.map(childNode => appendChildToNode(childNode, nodeId, data));
}
return { ...node, children };
}
Update: The above function uses ES6 spread syntax to append items. If you need to support older browsers w/o transpilation, you can use this updated version using Object.assign and Array#concat.
function appendChildToNode(node, nodeId, data) {
if (!node) {
return node;
}
var children;
if (node.id === nodeId) {
children = node.children.concat([data]);
} else {
children = node.children.map(childNode => appendChildToNode(childNode, nodeId, data));
}
return Object.assign({}, node, { children });
}
Let's find the node with ID 3 in our state, searching one level deep. Take the children of the current node, and find the node with correct ID within those children:
id = 3
node3 = state.children.find(v => v == id)
In that node, find ID 4. Now we're searching in the children of node 3:
id = 4 // ┌ act on node3!
node4 = node3.children.find(v => v == id)
Fitting that to Array.reduce(), the accumulator is the current node. It starts at the root node state, and then traverses the tree downwards: each time, we traverse the tree one level, using the next ID from a list of IDs. We need the recursion to start at the root of the tree, so the initial value is state, our root node.
If we take the above examples and reduce them:
[3, 4].reduce((acc, x) => acc.children.find(v => v === x), state)
// ids traverse one level start at root node
Unrolling it, this is equivalent to:
(state.children.find(v => v === 3)).children.find(v => v === 4)
The general form becomes:
const recursiveTraversal = ids =>
ids.reduce((acc, x) => acc.children.find(v => v === x), state)
Okay, so I am trying to create a function that allows you to input an array of Objects and it will return an array that removed any duplicate objects that reference the same object in memory. There can be objects with the same properties, but they must be different in-memory objects. I know that objects are stored by reference in JS and this is what I have so far:
const unique = array => {
let set = new Set();
return array.map((v, index) => {
if(set.has(v.id)) {
return false
} else {
set.add(v.id);
return index;
}
}).filter(e=>e).map(e=>array[e]);
}
Any advice is appreciated, I am trying to make this with a very efficient Big-O. Cheers!
EDIT: So many awesome responses. Right now when I run the script with arbitrary object properties (similar to the answers) and I get an empty array. I am still trying to wrap my head around filtering everything out but on for objects that are referenced in memory. I am not positive how JS handles objects with the same exact key/values. Thanks again!
Simple Set will do the trick
let a = {'a':1}
let b = {'a': 1,'b': 2, }
let c = {'a':1}
let arr = [a,b,c,a,a,b,b,c];
function filterSameMemoryObject(input){
return new Set([...input])
}
console.log(...filterSameMemoryObject(arr))
I don't think you need so much of code as you're just comparing memory references you can use === --> equality and sameness .
let a = {'a':1}
console.log(a === a ) // return true for same reference
console.log( {} === {}) // return false for not same reference
I don't see a good reason to do this map-filter-map combination. You can use only filter right away:
const unique = array => {
const set = new Set();
return array.filter(v => {
if (set.has(v.id)) {
return false
} else {
set.add(v.id);
return true;
}
});
};
Also if your array contains the objects that you want to compare by reference, not by their .id, you don't even need to the filtering yourself. You could just write:
const unique = array => Array.from(new Set(array));
The idea of using a Set is nice, but a Map will work even better as then you can do it all in the constructor callback:
const unique = array => [...new Map(array.map(v => [v.id, v])).values()]
// Demo:
var data = [
{ id: 1, name: "obj1" },
{ id: 3, name: "obj3" },
{ id: 1, name: "obj1" }, // dupe
{ id: 2, name: "obj2" },
{ id: 3, name: "obj3" }, // another dupe
];
console.log(unique(data));
Addendum
You speak of items that reference the same object in memory. Such a thing does not happen when your array is initialised as a plain literal, but if you assign the same object to several array entries, then you get duplicate references, like so:
const obj = { id: 1, name: "" };
const data = [obj, obj];
This is not the same thing as:
const data = [{ id: 1, name: "" }, { id: 1, name: "" }];
In the second version you have two different references in your array.
I have assumed that you want to "catch" such duplicates as well. If you only consider duplicate what is presented in the first version (shared references), then this was asked before.
I'm working on a table planner app where guests can be assigned to dinner tables.
I have created an object array in the state called tabledata, which will contain objects like so:
this.state = {
tabledata: [
{
name: "Top Table",
guests: ["guest1", "guest2", "guest3"]
},
{
name: "Table One",
guests: ["guest3", "guest4", "guest5"]
}
]
}
I am then creating a drag and drop interface where guests can move between tables. I have attempted to update the state like so:
updateTableList (tablename, guest) {
const selectedTableObj = this.state.tabledata.filter((tableObj) => tableObj.name === tablename);
const otherTableObjs = this.state.tabledata.filter((tableObj) => tableObj.name !== tablename);
selectedTableObj[0].guests.push(guest);
const updatedObjectArray = [...otherTableObjs, selectedTableObj];
this.setState({
tabledata: [...otherTableObjs, ...selectedTableObj]
});
}
This works but because I am removing selectedTableObj from the state and then adding it to the end of the array I'm getting some funky results on screen. The updated table always goes to the bottom of the page (as you'd expect).
How can I update the object without changing its position within the array?
Find the index of the table you want to update using Array.findIndex(). Create a new tabledata array. Use Array.slice() to get the items before and after the updated table, and spread them into the new tabledata array. Create a new table object using object spread, add the updated guests array, and add the table object between the previous items:
Code (not tested):
updateTableList(tablename, guest) {
this.setState((prevState) => {
const tableData = prevState.tabledata;
const selectedTableIndex = tableData.findIndex((tableObj) => tableObj.name === tablename);
const updatedTable = tableData[selectedTableIndex];
return {
tabledata: [
...prevState.tabledata.slice(0, selectedTableIndex),
{
...updatedTable,
guests: [...updatedTable.guests, guest]
},
...prevState.tabledata.slice(selectedTableIndex + 1)
]
};
});
}
selectedTableObj[0].guests.push(guest) directly mutates the state which is not encouraged in React.
Try this:
this.setState((prevState) => {
const newData = [...prevState.tabledata];
// if you pass in `index` as argument instead of `tablename` then this will not be needed
const index = prevState.tabledata.findIndex(table => tableObj.name === tablename);
newData[index] = {
...newData[index],
guests: newData[index].guests.concat([guest]),
};
return { tabledata: newData };
});
You also did not remove the guest from its previous table so you need to modify for that.
You can do it with a Array.reduce
let newState = this.state
// let newState = {...this.state} // in case you want everything immutable
newState.tableData = newState.tableData.reduce((acc, table) =>
if(table.name === tableName) {
return acc.concat({...table, guests: table.guests.concat(newGuest)})
} else {
return acc.concat(table)
}
)
I have an array of objects that can be of any length and any depth. I need to be able to find an object by its id and then modify that object within the array. Is there an efficient way to do this with either lodash or pure js?
I thought I could create an array of indexes that led to the object but constructing the expression to access the object with these indexes seems overly complex / unnecessary
edit1; thanks for all yours replies I will try and be more specific. i am currently finding the location of the object I am trying to modify like so. parents is an array of ids for each parent the target object has. ancestors might be a better name for this array. costCenters is the array of objects that contains the object I want to modify. this function recurses and returns an array of indexes that lead to the object I want to modify
var findAncestorsIdxs = function(parents, costCenters, startingIdx, parentsIdxs) {
var idx = startingIdx ? startingIdx : 0;
var pidx = parentsIdxs ? parentsIdxs : [];
_.each(costCenters, function(cc, ccIdx) {
if(cc.id === parents[idx]) {
console.log(pidx);
idx = idx + 1;
pidx.push(ccIdx);
console.log(pidx);
pidx = findAncestorsIdx(parents, costCenters[ccIdx].children, idx, pidx);
}
});
return pidx;
};
Now with this array of indexes how do I target and modify the exact object I want? I have tried this where ancestors is the array of indexes, costCenters is the array with the object to be modified and parent is the new value to be assigned to the target object
var setParentThroughAncestors = function(ancestors, costCenters, parent) {
var ccs = costCenters;
var depth = ancestors.length;
var ancestor = costCenters[ancestors[0]];
for(i = 1; i < depth; i++) {
ancestor = ancestor.children[ancestors[i]];
}
ancestor = parent;
console.log(ccs);
return ccs;
};
this is obviously just returning the unmodified costCenters array so the only other way I can see to target that object is to construct the expression like myObjects[idx1].children[2].grandchildren[3].ggranchildren[4].something = newValue. is that the only way? if so what is the best way to do that?
You can use JSON.stringify for this. It provides a callback for each visited key/value pair (at any depth), with the ability to skip or replace.
The function below returns a function which searches for objects with the specified ID and invokes the specified transform callback on them:
function scan(id, transform) {
return function(obj) {
return JSON.parse(JSON.stringify(obj, function(key, value) {
if (typeof value === 'object' && value !== null && value.id === id) {
return transform(value);
} else {
return value;
}
}));
}
If as the problem is stated, you have an array of objects, and a parallel array of ids in each object whose containing objects are to be modified, and an array of transformation functions, then it's just a matter of wrapping the above as
for (i = 0; i < objects.length; i++) {
scan(ids[i], transforms[i])(objects[i]);
}
Due to restrictions on JSON.stringify, this approach will fail if there are circular references in the object, and omit functions, regexps, and symbol-keyed properties if you care.
See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_native_JSON#The_replacer_parameter for more info.
As Felix Kling said, you can iterate recursively over all objects.
// Overly-complex array
var myArray = {
keyOne: {},
keyTwo: {
myId: {a: '3'}
}
};
var searchId = 'myId', // Your search key
foundValue, // Populated with the searched object
found = false; // Internal flag for iterate()
// Recursive function searching through array
function iterate(haystack) {
if (typeof haystack !== 'object' || haystack === null) return; // type-safety
if (typeof haystack[searchId] !== 'undefined') {
found = true;
foundValue = haystack[searchId];
return;
} else {
for (var i in haystack) {
// avoid circular reference infinite loop & skip inherited properties
if (haystack===haystack[i] || !haystack.hasOwnProperty(i)) continue;
iterate(haystack[i]);
if (found === true) return;
}
}
}
// USAGE / RESULT
iterate(myArray);
console.log(foundValue); // {a: '3'}
foundValue.b = 4; // Updating foundValue also updates myArray
console.log(myArray.keyTwo.myId); // {a: '3', b: 4}
All JS object assignations are passed as reference in JS. See this for a complete tutorial on objects :)
Edit: Thanks #torazaburo for suggestions for a better code.
If each object has property with the same name that stores other nested objects, you can use: https://github.com/dominik791/obj-traverse
findAndModifyFirst() method should solve your problem. The first parameter is a root object, not array, so you should create it at first:
var rootObj = {
name: 'rootObject',
children: [
{
'name': 'child1',
children: [ ... ]
},
{
'name': 'child2',
children: [ ... ]
}
]
};
Then use findAndModifyFirst() method:
findAndModifyFirst(rootObj, 'children', { id: 1 }, replacementObject)
replacementObject is whatever object that should replace the object that has id equal to 1.
You can try it using demo app:
https://dominik791.github.io/obj-traverse-demo/
Here's an example that extensively uses lodash. It enables you to transform a deeply nested value based on its key or its value.
const _ = require("lodash")
const flattenKeys = (obj, path = []) => (!_.isObject(obj) ? { [path.join('.')]: obj } : _.reduce(obj, (cum, next, key) => _.merge(cum, flattenKeys(next, [...path, key])), {}));
const registrations = [{
key: "123",
responses:
{
category: 'first',
},
}]
function jsonTransform (json, conditionFn, modifyFn) {
// transform { responses: { category: 'first' } } to { 'responses.category': 'first' }
const flattenedKeys = Object.keys(flattenKeys(json));
// Easily iterate over the flat json
for(let i = 0; i < flattenedKeys.length; i++) {
const key = flattenedKeys[i];
const value = _.get(json, key)
// Did the condition match the one we passed?
if(conditionFn(key, value)) {
// Replace the value to the new one
_.set(json, key, modifyFn(key, value))
}
}
return json
}
// Let's transform all 'first' values to 'FIRST'
const modifiedCategory = jsonTransform(registrations, (key, value) => value === "first", (key, value) => value = value.toUpperCase())
console.log('modifiedCategory --', modifiedCategory)
// Outputs: modifiedCategory -- [ { key: '123', responses: { category: 'FIRST' } } ]
I needed to modify deeply nested objects too, and found no acceptable tool for that purpose. Then I've made this and pushed it to npm.
https://www.npmjs.com/package/find-and
This small [TypeScript-friendly] lib can help with modifying nested objects in a lodash manner. E.g.,
var findAnd = require("find-and");
const data = {
name: 'One',
description: 'Description',
children: [
{
id: 1,
name: 'Two',
},
{
id: 2,
name: 'Three',
},
],
};
findAnd.changeProps(data, { id: 2 }, { name: 'Foo' });
outputs
{
name: 'One',
description: 'Description',
children: [
{
id: 1,
name: 'Two',
},
{
id: 2,
name: 'Foo',
},
],
}
https://runkit.com/embed/bn2hpyfex60e
Hope this could help someone else.
I wrote this code recently to do exactly this, as my backend is rails and wants keys like:
first_name
and my front end is react, so keys are like:
firstName
And these keys are almost always deeply nested:
user: {
firstName: "Bob",
lastName: "Smith",
email: "bob#email.com"
}
Becomes:
user: {
first_name: "Bob",
last_name: "Smith",
email: "bob#email.com"
}
Here is the code
function snakeCase(camelCase) {
return camelCase.replace(/([A-Z])/g, "_$1").toLowerCase()
}
export function snakeCasedObj(obj) {
return Object.keys(obj).reduce(
(acc, key) => ({
...acc,
[snakeCase(key)]: typeof obj[key] === "object" ? snakeCasedObj(obj[key]) : obj[key],
}), {},
);
}
Feel free to change the transform to whatever makes sense for you!