JSFiddle - React, Tailwind, and code Playground

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.16/vue.min.js"></script>

<div id="app">
  <fieldset>
    <legend>CONTROLS</legend>
    <div>
      <input v-model="obj.a">
      obj.a
    </div>
    <div>
      <input v-model="obj.b.c.d">
      obj.b.c.d
    </div>
    <div>
      <button @click="toggleNestedProp">toggle</button>
      obj.b.c.e: {{ obj.b.c.e ?? '__no_value__' }}
    <div>
    <div>
      <input v-model.number="obj.f" type="number">
      obj.f
      <ul>
        <li v-for="(n, i) in obj.fValues">
          <input v-model="obj.fValues[i]">
          obj.fValues[{{ i }}]
        </li>
      </ul>
    </div>
  </fieldset>

  <fieldset>
    <legend>OBJ</legend>
    <pre>{{ obj }}</pre>
  </fieldset>

  <fieldset>
    <legend>LOG</legend>
    <button @click="changelog = []">clear</button>
    <table>
      <thead>
        <tr>
          <th>path</th>
          <th>type</th>
          <th>new</th>
          <th>old</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="row in changelog">
          <td v-for="n in row">{{ n ?? '__no_value__' }}</td>
        </tr>
      </tbody>
    <table>
  </fieldset>
</div>

CSS

* {
  font-family: monospace;
}

td {
  border: 1px solid silver;
  padding: 5px;
}

JavaScript

function diff(a, b, path = '') {
  if (!(a instanceof Object) || a.constructor !== b?.constructor) {
    return a === b ? [] : [ [ path, 'changed', a, b ] ];
  }

  const keysA = new Set(Object.keys(a));
  const keysB = new Set(Object.keys(b));
  const addKey = k => path ? `${path}.${k}` : k;

  return [
    ...Array.from(keysA.difference(keysB), k => [ addKey(k), 'added', a[k], , ]),
    ...Array.from(keysB.difference(keysA), k => [ addKey(k), 'deleted', , b[k] ]),
    ...Array.from(keysA.intersection(keysB), k => diff(a[k], b[k], addKey(k))).flat(),
  ];
}

new Vue({
  el: '#app',
  data: () => ({
    obj: {
      a: 69,
      b: {
        c: {
          d: 'hello, world!!',
        },
      },
      f: 0,
      fValues: [],
    },
    changelog: [],
  }),
  computed: {
    objCopy() {
      return structuredClone(this.obj);
    },
  },
  watch: {
    objCopy(newVal, oldVal) {
      this.changelog.unshift(...diff(newVal, oldVal));
    },
    'obj.f'(val) {
      val = this.obj.f = Math.max(0, val | 0);

      this.obj.fValues.push(...Array.from(
        { length: Math.max(0, val - this.obj.fValues.length) },
        () => Math.random() * 10 | 0
      ));

      this.obj.fValues.length = Math.min(val, this.obj.fValues.length);
    },
  },
  methods: {
    toggleNestedProp() {
      if (this.obj.b.c.e) {
        this.$delete(this.obj.b.c, 'e');
      } else {
        this.$set(this.obj.b.c, 'e', 187);
      }
    },
  },
});