1

I have a react update form and i wish to differentiate the form data and the current data to figure out what has changed dynamically.

Problem summarized

find the minimum differences between 2 nested objects. And output an array of changed properties paths. E.g. if department_id in the departments list at index 0 changes while the rest stays the same - the algorithm should output ['departments'].

Example Data:

My data usually comes in like this (This is a simplified form , but the data has varying depths as shown here):

{id:115,
 departments: [{
              department_id:1,
              department_name:"asd"}],
 name: 'Test project',

}

Lets say the user decides to add in a department to the object, i wish to be able to detect the changes this way:

changes = ['departments']

or if the user changes the name :

changes = ['name']

the additional challenge here is that i wish to use this function across my forms , which means that the comparing should be able to handle different keys and depths of data

Edit:

data1 :

creation_date: "2020-06-16"
customer_information: Array(1)
                     0: 1
project_status: 1
sales_department: 1
sales_project_name: "helloss2sasdssssssssssss"
userProfile: Array(2)
             0: 1

data2:

creation_date: "2020-06-16"
customer_information: Array(1)
                      0: 1

project_status: 1
sales_department: 1
sales_project_name: "helloss2"
userProfile: Array(2)
             0: 1
             1: 2

Function called here :

const data1 = action.original
const data2 = action.final
const difference = Object.keys(data1).filter((key)=>!walk(data1[key],data2[key]))
console.log(difference)

Here is the console log for difference :

[] 

Expected:

['userProfile' , 'sales_project_name']
Community
  • 1
  • 1
neowenshun
  • 860
  • 1
  • 7
  • 21
  • needs more context. you could just use a deepEquals library. how much nesting do you have? are you keeping the data fully immutable, so you can just compare references instead of having to deep equals? what problem are you actually solving? there may be a better way to achieve your goal – user120242 Jun 16 '20 at 08:05
  • I think you should use a usePrevious hook, then you compare previous and current values then put changes in the array. – its_tayo Jun 16 '20 at 08:06
  • im using class based components across my entire project. I have the previous data and current data but i dont know the logic to compare these 2 sets of array – neowenshun Jun 16 '20 at 08:07
  • And how would you want the deeper levels to be returned? I mean, lets say you have `{ person: { name: 'changed-name' }}`, how would you want that to be listed in your changes? – Matthias S Jun 16 '20 at 08:09
  • I think you should use a deepEquals library like @user120242 suggested – its_tayo Jun 16 '20 at 08:10
  • @user120242 hello , im not familiar with deep equals . The problem im solving is actually dynamically generating notifcation messages based on the changes made to the form data . – neowenshun Jun 16 '20 at 08:10
  • @MatthiasS Hello , actualy , i would only require the field that was changed , so if the person changed i should store it like this : ['person'] , if departments changed , ['departments'] or if both changed ['person', 'department'] – neowenshun Jun 16 '20 at 08:13
  • I think that the problem can be stated as follows: find the minimum differences between 2 nested objects. And output an array of changed properties paths. E.g. if department_id in the departments list at index 0 changes while the rest stays the same - the algorithm should output ['departments[0].department_id']. – Daniil Andreyevich Baunov Jun 16 '20 at 08:15
  • Or we might look only into the leaf properties to simplify the task. Cuz it's ambiguous - when to consider that the whole 'departments[0]' object has changed, for example. – Daniil Andreyevich Baunov Jun 16 '20 at 08:17
  • @DaniilAndreyevichBaunov yes , i will add this to the question . However theres no need for me to know what the changed 'value' was as im only the key whos value changed. But i am interested to see the solution that includes both the key and value as well – neowenshun Jun 16 '20 at 08:18
  • I'm gonna think about the best solution. Actually, sounds like an interesting problem to solve. – Daniil Andreyevich Baunov Jun 16 '20 at 08:20
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/216039/discussion-between-daniil-andreyevich-baunov-and-neowenshun). – Daniil Andreyevich Baunov Jun 16 '20 at 08:21
  • Does this work: https://stackoverflow.com/a/40610459/11057988 – Rahul Bhobe Jun 16 '20 at 08:23

2 Answers2

2

Simple naive recursive function walk that deep equals and returns if the branch has changes. filters keys that match.

data1 = {
  creation_date: "2020-06-16",
  customer_information: [1],
  project_status: 1,
  sales_department: 1,
  sales_project_name: "helloss2sasdssssssssssss",
  userProfile: [1],
  version: 1
}
data2 = {

  creation_date: "2020-06-16",
  customer_information: [1],

  project_status: 1,
  sales_department: 1,
  sales_project_name: "helloss2",
  userProfile: [1, 2],
  version: 2
}

walk = (node1, node2) => {
  // different types, return false
  if (typeof node1 !== typeof node2) return false
  if (node1 && node2 && typeof node1 === 'object') {
    const keys = Object.keys(node1)
    // if type object, check same number of keys and walk on node1, node2
    return keys.length === Object.keys(node2).length &&
      keys.every(k => walk(node1[k], node2[k]))
  }
  // not object and types are same, return if node1 is equal to node2
  return node1 === node2
}
console.log(
  Object.keys(data1).filter((key) => !walk(data1[key], data2[key]))
)
user120242
  • 14,918
  • 3
  • 38
  • 52
  • Hey! Sorry im a novice in JS , do you mind giving me a short walk through your code? – neowenshun Jun 16 '20 at 09:26
  • Sure thing. I've added comments to clarify what each part does, and added a fix for something I forgot to account for. – user120242 Jun 16 '20 at 09:34
  • Thanks , appreciate it. I seem to be receiving an error of Cannot convert undefined or null to object , but im not able to find out where exactly that happened – neowenshun Jun 16 '20 at 09:37
  • do you want null vs undefined properties to be treated as changes? – user120242 Jun 16 '20 at 09:43
  • yup , if the there was a value initially , but in the final data set the value became null , i would like to register it as a change – neowenshun Jun 16 '20 at 09:43
  • i mean if one of them is null but other one is not defined? anyways I've added truthy existence checks to account for it – user120242 Jun 16 '20 at 09:46
  • hms , i think im facing the same issue as the answer below , it seems im not able to detect the changes i made. I have a feeling that its because the function exits when it encounters a difference . Just to check , does the function stop running after it has detected one change? If so is it possible to allow the function to continue running even though it has detected a change? – neowenshun Jun 16 '20 at 09:48
  • Right, it just checks if any of the top level branches have any changes in its children nodes. Did you want the actual nested paths? – user120242 Jun 16 '20 at 09:49
  • Actually , i dont need the actual nested paths . i just require the parent branch to detect if there is any changes in its values . To refer to the example above , what i did was to chang the name of data1 , however , the function does not return ['name'] – neowenshun Jun 16 '20 at 09:56
  • I have an additional field called version that increments by 1 every time an update is called. And it is at the top place of my dataset. So when i changed the name , the function returned ['version'] instead of ['version' , 'name'] which led me to think that the function exited when it encounters a change – neowenshun Jun 16 '20 at 09:57
  • Sorry can you give me an example? I'm having trouble understanding what you mean. Do you mean the variable names changing? In the example above, name is already different, so the output includes name – user120242 Jun 16 '20 at 09:59
  • Okay i will update the above question with the actual data set , and the value returned from the function – neowenshun Jun 16 '20 at 10:03
  • Hello , i have updated the question to include the data that im working with and the return value – neowenshun Jun 16 '20 at 10:09
  • can you console.log(data1,data2) and see if the objects are what you expect? I can't replicate the problem. I've updated my answer using your new datasets, but it seems to work as expected – user120242 Jun 16 '20 at 10:16
  • Apologies , i realize the data i passed in was incorrect, i figured out my own mistake and your solution is perfect, thanks! – neowenshun Jun 16 '20 at 10:25
0

If you don't want to use any library for comparing nested objects, you could simply convert to JSON and compare the strings.

Assuming you want to compare objects, it could look like this:

function getUnequalKeys(object1, object2) {
  let unequalKeys = [];
  for (let key in object1) {
    if (object1.hasOwnProperty(key) 
        && (
         !object2.hasOwnProperty(key)
         || JSON.stringify(object1[key]) !== JSON.stringify(object2[key])
        )
    ) {
      unequalKeys.push(key);
    }
  }
  for (let key2 in object2) {
    if (object2.hasOwnProperty(key2) && !object1.hasOwnProperty(key2)) {
      unequalKeys.push(key2);
    }
  }
  return unequalKeys;
}

This would return all first-level keys that don't exist in both objects or have different values.

EDIT: What it essentially does is the following: Loop through each key in object1. Check if that same key exists in object2. If it does not exist, it means the key is not equal, so the condition is true and the key is added to the list of unequal keys. If the key does exist in object2, make a JSON string of both values and compare those strings. If the strings are not the same, it means they have different values. In that case, also add the key to the array of unequal keys. Now we already checked all keys of object1.

As a last step, go through all keys of object2 and check if they are not present in object1, and in that case also add them to the array of unequal keys.

Matthias S
  • 3,358
  • 3
  • 21
  • 31
  • Hmms im still trying to digest the code. Meanwhile , im having a syntax error of SyntaxError: Unexpected token p in JSON at position 0 – neowenshun Jun 16 '20 at 08:45
  • Thanks I had to stringify instead of parse, stupid mistake :-) will add an edit with explanation – Matthias S Jun 16 '20 at 08:57
  • thanks , however is it possible to ammend the code such that it checks the value instead of the key. From what i understand , the code checks if the keys are the same , however in my form data and current data , the keys dont change , only the values – neowenshun Jun 16 '20 at 09:04
  • But if the values changed , i would like to return the keys back whose values changed – neowenshun Jun 16 '20 at 09:05
  • Ah okay , my bad . I have tried out the code , it doesnt seem to return the changes for the values. What i did was to ammend the name field in the dataset above – neowenshun Jun 16 '20 at 09:07
  • Had a little typo from editing in there. The problem with JSON.stringify is that if you have a nested object e.g. `{person: {name: 'neo', age: 25}}` as one object and `{person: {age: 25, name: 'neo'}}` as another, it would not be equal, because the order of keys is different. Since JSON.stringify makes the object a string and then compares, the text will not be the same if the order is not the same. – Matthias S Jun 16 '20 at 09:09
  • Honestly @user120242 answer is way more elegant, I would advise you to use his/her solution if order doesn't matter to you :). – Matthias S Jun 16 '20 at 09:14