0

I need to search and update recursively in the JSON. However, tried many ways to break it, but couldn't succeed. Below is my sample code trying to match the id and update the title to the matched id. Any help will be appreciated. TIA

The result that I'm getting is default value i.e. false

var data = {
      treeData: [
        { title: 'United States of America', children: [{ title: 'Chicago', id:4, editOn:false, children:[{ title: 'New Mexico', id:17, editOn:false  }]  },{ title: 'New York', id:3, editOn:true  }], id:0, editOn:false },
        { title: 'United Arab Emirate', children: [{ title: 'Abu Dhabi', id:5, editOn:true  }], id:1, editOn:true  },
        { title: 'United Kingdom', children: [{ title: 'London', id:7, editOn:false  },{ title: 'Hampshire', id:6, editOn:true  }], id:2, editOn:false  },
      ]
    };
    

function findAndUpdate (id , data, title) {
    var status = false;
    data.map(function(array,index){
        console.log(array.id, id, "children" in array, status)
        if( array.id === id ) {
            console.log("Match Found");
            status = true;
            return status;
        } 
        
        if( "children" in array ){
             return findAndUpdate( id, array.children, title)
        }
    });
  return status;

}

var s = findAndUpdate(5, data.treeData, "India");
console.log(s)
MD Danish
  • 179
  • 1
  • 10
  • Where is function - fnd? – Steve Tomlin Dec 30 '20 at 12:51
  • 1
    `data.map` produces an array, but you're not doing anything with it. Not storing it in a variable, not returning it either. It just sits there. Also, no idea what `fnd` is – Jeremy Thille Dec 30 '20 at 12:54
  • If you mean fnd == findAndUpdate then you have to return the data.map as the return of your function. Also in your map method you have to return something if the first 2 if conditions don't match. – Steve Tomlin Dec 30 '20 at 12:54
  • I would also advise you change data.map to data.forEach since you aren't mapping any data with it. – Steve Tomlin Dec 30 '20 at 12:56

4 Answers4

3

mutation

If the goal is to mutate the original data, this can be done in a simplistic way. Notice our mutate function has no knowledge of the shape of your input data, .treeData, or .children properties. In addition, it works uniformly on both objects and arrays -

const data =
  {treeData:[{title:'United States of America',children:[{title:'Chicago',id:4,editOn:false,children:[{title:'New Mexico',id:17,editOn:false}]},{title:'New York',id:3,editOn:true}],id:0,editOn:false},{title:'United Arab Emirate',children:[{title:'Abu Dhabi',id:5,editOn:true}],id:1,editOn:true},{title:'United Kingdom',children:[{title:'London',id:7,editOn:false},{title:'Hampshire',id:6,editOn:true}],id:2,editOn:false}]}
    
function mutate (t, f) {
  switch (t?.constructor) {
    case Object:
    case Array:
      f(t)
      Object.values(t).forEach(v => mutate(v, f))
  }
}

mutate(data, function (t) {
  if (t.id === 5)
    t.title = "India"
})

console.log(JSON.stringify(data, null, 2))
.as-console-wrapper {max-height: 100% !important; top: 0}

An important caveat, as noted by Scott, if id == 5 multiple places in the tree, all instances will be updated.

If you still find it necessary to write findAndUpdate, we can easily wrap mutate as follows -

const findAndUpdate = (id, title, data) =>
  mutate(data, function (t) {
    if (t.id === id)
      t.title = title
  })

persistence

But mutation is the source of many bugs and programmer headaches, and so I avoid it where possible. In the program below we do not mutate the original input and instead generate a new object output. Here's a generic update which has a unique but intuitive interface. Again, our function has no opinions about the shape of the input data -

const update = (t, f) =>
  isObject(t)
    ? f(t, r => Object.assign(isArray(t) ? [] : {}, t, r))
        ?? map(t, v => update(v, f))
    : t

const result =
  update(data, function (t, assign) {
    if (t?.id === 5)
      return assign({ title: "India" })
  })

It depends on a generic map function which was written in another Q&A -

const map = (t, f) =>
  isArray(t)
    ? t.map((v, k) => f(v, k))
: isObject(t)
    ? Object.fromEntries(Object.entries(t).map(([k, v]) =>  [k, f(v, k)]))
: t

As well as some readable type predicates -

const isArray = t =>
  Array.isArray(t)

const isObject = t =>
  Object(t) === t

If you still find it necessary to write findAndUpdate, we can easily wrap update as follows -

const findAndUpdate = (id, title, data) =>
  update(data, function (t, assign) {
    if (t?.id === id)
      return assign({ title })
  })

Expand the snippet below to verify the results of update in your own browser -

const data =
  {treeData:[{title:'United States of America',children:[{title:'Chicago',id:4,editOn:false,children:[{title:'New Mexico',id:17,editOn:false}]},{title:'New York',id:3,editOn:true}],id:0,editOn:false},{title:'United Arab Emirate',children:[{title:'Abu Dhabi',id:5,editOn:true}],id:1,editOn:true},{title:'United Kingdom',children:[{title:'London',id:7,editOn:false},{title:'Hampshire',id:6,editOn:true}],id:2,editOn:false}]}

const isArray = t =>
  Array.isArray(t)

const isObject = t =>
  Object(t) === t

const map = (t, f) =>
  isArray(t)
    ? t.map((v, k) => f(v, k))
: isObject(t)
    ? Object.fromEntries(Object.entries(t).map(([k, v]) =>  [k, f(v, k)]))
: t

const update = (t, f) =>
  isObject(t)
    ? f(t, r => Object.assign(isArray(t) ? [] : {}, t, r))
      ?? map(t, v => update(v, f))
    : t

const result =
  update(data, function (t, assign) {
    if (t?.id === 5)
      return assign({ title: "India" })
  })

console.log(JSON.stringify(result, null, 2))
.as-console-wrapper {max-height: 100% !important; top: 0}

result

Aside from mutate which modifies the original and update which generates a new object, each program produces the same result. Some newline characters have been removed to condense output -

{ "treeData": [
    { "title": "United States of America",
      "children": [
        {
          "title": "Chicago",
          "id": 4,
          "editOn": false,
          "children": [
            { "title": "New Mexico", "id": 17, "editOn": false }
          ]
        },
        { "title": "New York", "id": 3, "editOn": true }
      ],
      "id": 0,
      "editOn": false
    },
    { "title": "United Arab Emirate",
      "children": [
        { "title": "India", "id": 5, "editOn": true }  // <--
      ],
      "id": 1,
      "editOn": true
    },
    { "title": "United Kingdom",
      "children": [
        { "title": "London", "id": 7, "editOn": false },
        { "title": "Hampshire", "id": 6, "editOn": true }
      ],
      "id": 2,
      "editOn": false
    }
  ]
}

it's a plain object

I need to search and update recursively in the JSON...

I can't finish this answer without remarking you want to search and update a JavaScript Object. JSON, JavaScript Object Notation, is a data format that represents JavaScript values as strings. JSON is always a string.

Never attempt to modify a JSON string. Instead, always -

  1. decode the JSON string into a JavaScript value using JSON.parse
  2. modify the value
  3. re-encode the value as JSON using JSON.stringify

You don't need to worry about JSON here because you're already starting with a JavaScript Object.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Another wonderful answer! And thank you for taking on `JSON`. I keep meaning to make that same point. We should probably find a definitive Q+A about that to link to. I want to point out that there might be a problem specifically because you *don't* handle `children`. If, for instance, the first item in `treeData` had a property, `foo: [..., {id:4, val: 'd'}, {id: 5, val: 'e'}, ...]`, you would add `title: 'India'` to one of those, which presumably is not what's intended. I always end up with different techniques to descend key-value object trees and ones which define `children` explicitly. – Scott Sauyet Dec 31 '20 at 17:03
  • 1
    true, `id == 5` could appear multiple times in the tree, and it's a serious concern if sequential ids are used for various object types. however traversing on a known key, `.children` for example, does not mitigate the problem entirely. ie, which node should be updated in the following tree? `{ name: "root", children: [ { name: "people", children: [ { id: 5, name: "alice", children: ... } ] }, { name: "companies", children: [ { id: 5, name: "acme corp", children: ... } ] }, ... ] }` - perhaps a uuid (or similar) is best for this type of program? as always, thanks for the discussion. – Mulan Dec 31 '20 at 17:33
  • 1
    Yes, trying to write really generic code can lead us down rabbit holes. Without guidance from the OP, we can never really know what the data design looks like, and different guesses lead to different rabbit holes. If we go down deep enough, they'll probably reconnect. ;-) – Scott Sauyet Dec 31 '20 at 18:38
  • 1
    Scott Sauyet, Thank You. Really liked the discussion and different pro approaches to the problem. – MD Danish Jan 03 '21 at 15:25
2

main problem was that you used a map while you want to use a reduce - which also iterates over the elements of an array, but it aggregates them to a singular result.

I updated some of your Syntax to ES6 (since .map worked there should be no problem with that.

Also note, that I already inserted a line to change the title, which wasn't in there originally and maybe will need to be removed / have a condition in regard to editOn

const data = {
      treeData: [
        { title: 'United States of America', children: [{ title: 'Chicago', id:4, editOn:false, children:[{ title: 'New Mexico', id:17, editOn:false  }]  },{ title: 'New York', id:3, editOn:true  }], id:0, editOn:false },
        { title: 'United Arab Emirate', children: [{ title: 'Abu Dhabi', id:5, editOn:true  }], id:1, editOn:true  },
        { title: 'United Kingdom', children: [{ title: 'London', id:7, editOn:false  },{ title: 'Hampshire', id:6, editOn:true  }], id:2, editOn:false  },
      ]
    };
    

findAndUpdate = (id, data, title) => {
    const status = data.reduce((total, current) => {
        if( current.id === id ) {
            console.log('found');
            /*** edit title takes place here ***/
            current.title = title;
            // return true if id was found
            return true;
        } 
        
        if ("children" in current ) {
            // return true if status already was true otherwise search in children
            return total || findAndUpdate( id, current.children, title)
        }
    }, false); // initially false
  return status;
}

const s = findAndUpdate(5, data.treeData, "India");
console.log(s)
  • you can think of `.reduce` as a sort of bare-metal version of `.map` - it has greater capability but also greater conceptual overhead and risk. i wouldn't say that _"main problem was that you used a map..."_, you can see the other answers here accomplish the goal using `.map` - not using `.reduce`. – Mulan Dec 31 '20 at 16:34
2

First pass

For a first pass at this problem, I would write a generic helper function, updateWhen that would work with arrays like your treeData node, ones that contains items that we might want to transform, some including children nodes on which we would like to recur.

This function takes two callback functions, the first a predicate which tests whether we want to change this node and the second, the function that actually changes it. It then recurs on the children node if the object has one.

We can then write findAndUpdate based on that. As well as calling the updateWhen appropriately, it handles the (to my mind odd1) parameter order requested, and it handles the fact that treeData is not the root of the object you would like to pass.

It's important to note that this does not alter your input. It returns a new object with the changes made. A fan of functional programming, I find this a very important principle to follow.

Here's one implementation:

const updateWhen = (pred, change) => (xs) =>
  xs .map (({children, ...rest}) => ({
    ... (pred (rest) ? change (rest) : rest),
    ... (children ? {children:  updateWhen (pred, change) (children)} : {})
  }))

const findAndUpdate = (id, {treeData, ...rest}, title) => ({
  treeData: updateWhen (
    ({id: i}) => i === id,
    o => ({...o, title})
  ) (treeData), 
  ... rest
})

const  data = {treeData: [{title: "United States of America", children: [{title: "Chicago", id: 4, editOn: !1, children: [{title: "New Mexico", id: 17, editOn: !1}]}, {title: "New York", id: 3, editOn: !0}], id: 0, editOn: !1}, {title: "United Arab Emirate", children: [{title: "Abu Dhabi", id: 5, editOn: !0}], id: 1, editOn: !0}, {title: "United Kingdom", children: [{title: "London", id: 7, editOn: !1}, {title: "Hampshire", id: 6, editOn: !0}], id: 2, editOn: !1}]}

console .log (
  findAndUpdate (5, data, 'India')
)
.as-console-wrapper {max-height: 100% !important; top: 0}

Adding helpers

The functions passed to updateWhen in the implementation above are not horribly unreadable. But with helper functions, they might be made much easier to understand.

With that in mind, we can write a generic, reusable propEq function that takes a property name and a test value and returns a function, that, given an object, returns whether its value for that property name matches the test value.

And we can write setProp, which takes a property name and a new value and returns a function that given an object, returns a shallow clone of that object, with the value for that property name set to our new value.

With those, the code becomes more readable:

const findAndUpdate = (id, {treeData, ...rest}, title) => ({
  treeData: updateWhen (propEq ('id', id), setProp ('title', title)) (treeData), 
  ... rest
})

You can see it in action in this snippet:

const updateWhen = (pred, change) => (xs) =>
  xs .map (({children, ...rest}) => ({
    ... (pred (rest) ? change (rest) : rest),
    ... (children ? {children:  updateWhen (pred, change) (children)} : {})
  }))

const propEq = (prop, val) => ({[prop]: p}) =>
  p == val

const setProp = (prop, val) => ({[prop]: p, ...rest}) =>
  ({[prop]: val, ...rest})

const findAndUpdate = (id, {treeData, ...rest}, title) => ({
  treeData: updateWhen (propEq ('id', id), setProp ('title', title)) (treeData), 
  ... rest
})


const  data = {treeData: [{title: "United States of America", children: [{title: "Chicago", id: 4, editOn: !1, children: [{title: "New Mexico", id: 17, editOn: !1}]}, {title: "New York", id: 3, editOn: !0}], id: 0, editOn: !1}, {title: "United Arab Emirate", children: [{title: "Abu Dhabi", id: 5, editOn: !0}], id: 1, editOn: !0}, {title: "United Kingdom", children: [{title: "London", id: 7, editOn: !1}, {title: "Hampshire", id: 6, editOn: !0}], id: 2, editOn: !1}]}

console .log (
  findAndUpdate (5, data, 'India')
)
.as-console-wrapper {max-height: 100% !important; top: 0}

Using Ramda

Finally, I'm a founder of Ramda and I find it useful for such data transformation projects. Ramda's functions were the inspiration for propEq and for setProp (although in Ramda the latter is called assoc.) Ramda also has a function, evolve, which takes an object which specifies how to alter certain properties in what is otherwise a clone of the object. It's useful to deal with something like your treeData property, while leaving the rest of the outer data object alone. With these Ramda functions, I would use the same updateWhen, but use this fairly simple version of findAndUpdate 2:

const findAndUpdate = (id, data, title) => evolve ({
  treeData: updateWhen (propEq ('id', id), assoc ('title', title)), 
}, data)

It would look like this:

const updateWhen = (pred, change) => (xs) =>
  xs .map (({children, ...rest}) => ({
    ... (pred (rest) ? change (rest) : rest),
    ... (children ? {children:  updateWhen (pred, change) (children)} : {})
  }))

const findAndUpdate = (id, data, title) => evolve ({
  treeData: updateWhen (propEq ('id', id), assoc ('title', title)), 
}, data)


const  data = {treeData: [{title: "United States of America", children: [{title: "Chicago", id: 4, editOn: !1, children: [{title: "New Mexico", id: 17, editOn: !1}]}, {title: "New York", id: 3, editOn: !0}], id: 0, editOn: !1}, {title: "United Arab Emirate", children: [{title: "Abu Dhabi", id: 5, editOn: !0}], id: 1, editOn: !0}, {title: "United Kingdom", children: [{title: "London", id: 7, editOn: !1}, {title: "Hampshire", id: 6, editOn: !0}], id: 2, editOn: !1}]}

console .log (
  findAndUpdate (5, data, 'India')
)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js"></script>
<script> const {evolve, propEq, assoc} = R                                 </script>

While we could write a one-off version of evolve, that is a more complex function and we probably do well to use a more battle-hardened version or it.

The advantage of abstraction

Breaking the problem up this way has several advantages. First, although we have more functions, each one is simpler, focused on a single goal. This tends to make code much more readable, so long as those functions are well-named.

Second, we can reuse our three helper functions, updateWhen, propEq, and setProp/assoc across other parts of our current project and in other projects. Keeping a personal list of such useful functions can make future projects much easier to code.


1 It makes much more sense to me to have the id and title parameters together. They define the change to be made, and placing the data parameter between them makes it less readable. I would actually go further, and prefer that supplying the id and title returns you a function that takes the data parameter, so that you'd call it like

findAndUpdate (5, 'India') (data)

Perhaps that intermediate function would never be helpful to you. I often find them useful. But either of these ways to call it is still an improvement:

findAndUpdate (5, 'India', data)
// or
findAndUpdate (data, 5, 'India')

2 Some Ramda users might go further and push this to a fully point-free solution, using also the simple objOf and the more obscure useWith:

const findAndUpdate = pipe (
  useWith (updateWhen, [propEq ('id'), assoc ('title')]),
  objOf ('treeData'),
  evolve
)

which would then be called like findAndUpdate (5, 'India') (data). While I prefer that argument order, and possibly the two-level call, the code is to me somewhat more obscure than the one above.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • A great example of refactoring and composition of generic functions. And a shining example of Ramda's strengths. My one complaint is that the function specifically operates on the `.children` property, making it feel a touch clumsy. And an inconsequential tweak to the original, `({title: t, ...o}) => ({title, ...o})` can be rewritten as `o => ({ ...o, title })`. – Mulan Dec 31 '20 at 16:28
  • Working on `.children` is very intentional. We could pass a property name as an initial parameter, or better, a function, but then we're much on our way to a different style of solution, one we've both used and discussed here; that's powerful and useful, but perhaps overkill in this case. (I'll comment more on `.children` for your answer.) And I will add your `title` tweak. I thought the original had some pedagogical value, but I never did bother commenting on it. – Scott Sauyet Dec 31 '20 at 16:53
1

Here is a solution using object-scan. It is a more flexible solution (i.e. if you wanted to search for children in other paths etc), but there is obviously a trade-off in introducing a dependency

// const objectScan = require('object-scan');

const myData = { treeData: [{ title: 'United States of America', children: [{ title: 'Chicago', id: 4, editOn: false, children: [{ title: 'New Mexico', id: 17, editOn: false }] }, { title: 'New York', id: 3, editOn: true }], id: 0, editOn: false }, { title: 'United Arab Emirate', children: [{ title: 'Abu Dhabi', id: 5, editOn: true }], id: 1, editOn: true }, { title: 'United Kingdom', children: [{ title: 'London', id: 7, editOn: false }, { title: 'Hampshire', id: 6, editOn: true }], id: 2, editOn: false }] };

const findAndUpdate = (id, tree, title) => objectScan(['treeData.**(^children$).id'], {
  rtn: 'bool',
  abort: true,
  useArraySelector: false,
  filterFn: ({ parent, value }) => {
    if (value === id) {
      parent.title = title;
      return true;
    }
    return false;
  }
})(tree);

console.log(findAndUpdate(5, myData, 'India')); // returns true iff found
// => true

console.log(myData);
// => { treeData: [ { title: 'United States of America', children: [ { title: 'Chicago', id: 4, editOn: false, children: [ { title: 'New Mexico', id: 17, editOn: false } ] }, { title: 'New York', id: 3, editOn: true } ], id: 0, editOn: false }, { title: 'United Arab Emirate', children: [ { title: 'India', id: 5, editOn: true } ], id: 1, editOn: true }, { title: 'United Kingdom', children: [ { title: 'London', id: 7, editOn: false }, { title: 'Hampshire', id: 6, editOn: true } ], id: 2, editOn: false } ] }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan@13.7.1"></script>

Disclaimer: I'm the author of object-scan

vincent
  • 1,953
  • 3
  • 18
  • 24