Utils (@pim.sk/utils)

recursiveCompare

Compares two objects or arrays recursively and returns a list of changed fields. Each change contains the key path k, original value f (from) and new value t (to). Useful for form change detection and generating minimal patch payloads.
import recursiveCompare from '@pim.sk/utils/recursiveCompare.mjs'

Usage

recursiveCompare( before, after )

Compares two objects and returns an array of changed fields. Each item contains k (key path), f (from — original value) and t (to — new value). Returns [] if nothing changed.

recursiveCompare(
    { a: 1, b: 2,   c: "hello" },   // before
    { a: 1, b: 5,   c: "world" }    // after
)
// → [
//     { k: "b", f: 2,       t: 5       },
//     { k: "c", f: "hello", t: "world" },
// ]

recursiveCompare( { a: 1 }, { a: 1 } )
// → []   no changes
recursiveCompare(
    { a: 1, b: 2,   c: "hello" },
    { a: 1, b: 5,   c: "world" }
)
// → [
//     { k: "b", f: 2,       t: 5       },
//     { k: "c", f: "hello", t: "world" },
// ]

Nested objects — dot notation path

Recursion traverses nested objects at any depth. The key path k uses dot notation to show where the change occurred.

const before = {
    count: 3,
    user: {
        name: "John",
        address: { city: "Kosice", zip: "040 01" }
    }
}
const after = {
    count: 5,
    user: {
        name: "John",
        address: { city: "Presov", zip: "080 01" }
    }
}

recursiveCompare( before, after )
// → [
//     { k: "count",                f: 3,        t: 5        },
//     { k: "user.address.city",    f: "Kosice", t: "Presov" },
//     { k: "user.address.zip",     f: "040 01", t: "080 01" },
// ]
recursiveCompare( before, after )
// → [
//     { k: "count",             f: 3,        t: 5        },
//     { k: "user.address.city", f: "Kosice", t: "Presov" },
//     { k: "user.address.zip",  f: "040 01", t: "080 01" },
// ]

Arrays — index as path key

Array items are compared by index. The path uses the index as the key. If the arrays themselves change type/length at the top level, the whole array is stringified in f and t.

// items in array changed — path uses index:
recursiveCompare(
    { tags: ["js", "php", "css"] },
    { tags: ["js", "vue", "css"] }
)
// → [{ k: "tags.1", f: "php", t: "vue" }]

// array replaced entirely (top-level array vs non-array):
recursiveCompare(
    { ids: [1, 2, 3] },
    { ids: [1, 2, 3, 4] }
)
// → [{ k: "ids.3", f: undefined, t: 4 }]
// item changed — path = "tags.1" (index 1)
recursiveCompare(
    { tags: ["js", "php", "css"] },
    { tags: ["js", "vue", "css"] }
)
// → [{ k: "tags.1", f: "php", t: "vue" }]

Use case — form change detection

Store the original form data on load, compare with current values on save. Only changed fields are sent to the server.

// on page load — save original state
const original = { name: "John", email: "j@x.com", role: "user" }

// on save — compare with edited values
const edited = { name: "Jane", email: "j@x.com", role: "admin" }

const changes = recursiveCompare(original, edited)
// → [
//     { k: "name", f: "John",  t: "Jane"  },
//     { k: "role", f: "user",  t: "admin" },
// ]

// send only changed fields:
const patch = Object.fromEntries( changes.map(c => [c.k, c.t]) )
// → { name: "Jane", role: "admin" }
const original = { name: "John", email: "j@x.com", role: "user" }
const edited   = { name: "Jane", email: "j@x.com", role: "admin" }

const changes = recursiveCompare(original, edited)
// → [{ k:"name", f:"John", t:"Jane" }, { k:"role", f:"user", t:"admin" }]

// build patch — only changed values:
const patch = Object.fromEntries( changes.map(c => [c.k, c.t]) )
// → { name: "Jane", role: "admin" }
v 1.1.2