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.