0

I have a component that renders a table of Inventoried computer equipment. Here is the relevant code for initial render:

let oEquiptByType = reactive({
  Laptop: [],
  iPad: [],
  "Document Camera": [],
  "Overhead Projector": [],
  Chromebook: [],
  Desktop: [],
  MacBook: [],
  Scanner: [],
});

// ======== Props =========== //
const props = defineProps({
  propFormData: {},
});

// Now let's use Stein to retrieve the SS data
// eslint-disable-next-line no-unused-vars
const fetchSheetsData = function () {
  const store = new SteinStore(
    "https://api.steinhq.com/v1/storages/618e81028d29ba2379044caa"
  );
  store
    .read("HS - Classrooms")
    .then((data) => {
      scrapDataHSClassrooms.value = data;
      emptyRowsRemoved.value.forEach((item) => {
        // Let's construct an object that separates equipment by type
        // Check if property exists on oEquiptByType object
        const exists = Object.prototype.hasOwnProperty.call(
          oEquiptByType,
          item["Equipment"]
        );

        // If item(row) is good lets push the row onto the corresponding Object Array
        // in oEquiptByType. This will construct an object where each object property corresponds
        // to an equipment category. And each oEquiptByType entry is an array where each array
        // element is a row from the SS. e.g., oEquiptByType["Laptop"][3] is a row from
        // SS and is a laptop.
        if (exists) {
          oEquiptByType[item["Equipment"]].push(item);
        }
      });
    })
    .catch((e) => {
      console.error(e);
      failure.value = true;
    });
};

// =============== Called on component mount =============================== //
onMounted(fetchSheetsData);

The initial render is fine. Now I have a watcher on the prop so when someone submits a new item for the inventory I push that data onto the corresponding object array (ie, a laptop would be pushed onto the oEquiptByType[props.propFormData.Equipment] via oEquiptByType[props.propFormData.Equipment].push(props.propFormData);

// ================================================================ //
// ======================= Watch effects ========================== //
// ================================================================ //
watch(props.propFormData, () => {
  // Push the submitted form item onto the reactive
  // oEquiptByType object array. This update of Vue state
  // will then be injected into DOM and automagically update browser display.
  oEquiptByType[props.propFormData.Equipment].push(props.propFormData);
});

This works fine for the first item I add to backend as you can see here with original and then adding first item :

Before first add

and after first item added (a laptop)

First Item added Notice the oEquiptByType[props.propFormData.Equipment] has the new item added. Great. But now when I add a second item (a MacBook) is added this is resulting state:

Macbook entered

Notice the Macbook array has been updated but also the Laptop array's last item has been overwritten with the Mac book entry??? And this behavior continues for any additional items added from a user. I have read docs over and do not see anything that would explain this behavior. I'm hoping maybe someone with more than my limited experience with Vue can help me out. Any additional info needed please let me know. Thanks...

Update: Put a JSON.Stringify in watch function

enter image description here

Update two: here is lineage of prop.FormData-

enter image description here

we start in form-modal and emit the form data like:

emit("emiterUIUpdate", formAsPlainObject);

then catch the data in the parent App.vue:

    <FormModal
        v-show="isModalVisible"
        @close="closeModal"
        @emiterUIUpdate="updateUI"
      >

  

<DisplayScrap :propFormData="formData" />

    const formData = reactive({});
    
// Method to be called when there is an emiterUIUpdate event emiited
// from form-modal.vue @param(data) is the form data sent from the
// form submission via the event bus. We will then send this data back
// down to child display-scrap component via a prop.
    const updateUI = (data) => {
      Object.assign(formData, data);
    };

and then as posted previous in display-scrap.vue the prop propFormData is defined and watched for in the watch function. hope that helps..

Alan
  • 1,067
  • 1
  • 23
  • 37
  • Impossible to answer without more info about where `props.propFormData` is exactly coming from (apart it is from parent component of course) and how the object is created. Most likely you are just reusing single object instance. Easy to test with simple comparison `Laptop[17] === Macbook[0]` – Michal Levý Jan 14 '22 at 17:55
  • See update 2 for further explanation. – Alan Jan 14 '22 at 19:23

2 Answers2

0

It seems like the watch is getting triggered more often than you expect.

Might be that changes to props.propFormData are atomic and every incremental change triggers changes to the props, which in turn triggers the watch.

Try console logging the value of props.propFormData with JSON.stringify to see what changes are triggering it.

Daniel
  • 34,125
  • 17
  • 102
  • 150
  • That was my thought also but see above update image. The watcher seems to fire only once for each submit. – Alan Jan 14 '22 at 18:58
0

What happens here:

  1. Your form modal emits the emiterUIUpdate event on Ok or Save (button)
  2. Parent takes the object emitted and use Object.assing to copy all properties of emitted object to a formData reactive object. Instead of creating completely new object, you are just replacing the values of all properties of that object all and over again
  3. The formData object is passed by a prop to child component and whenever it changes, it is pushed to target array

As a result, you have a multiple references to same object (formData hold by a parent component) and all those references are to same object in memory. Every Object.assign will overwrite properties of this object and all references will reflect those changes (because all references are pointing to the same object in memory)

Note that this has nothing to do with Vue reactivity - this is simple JavaScript - value vs reference

There is no clear answer to what to do. There are multiple options:

  1. Simplest (and not clearest)
  • just do not use Object.assign - create new object every time "Save" is clicked
  • change formData to a ref - const formData = ref({})
  • replace the value of that ref on emiterUIUpdate event - formData.value = { ...data }
  • your watch handler in the child will stop working because you are watching props in a wrong way - instead of watch(props.propFormData, () => { use watch(() => props.propFormData, () => {
  1. Better solution
  • the data should be owned by parent component
  • when modal emits new data (Save), Parent will just add the newly generated object into a list
  • share the data with DisplayScraps component using a prop (this can be a simple list or a computed creating object similar to oEquiptByType)
Michal Levý
  • 33,064
  • 4
  • 68
  • 86
  • I like second option but not clear what you mean by add new object into a list? And what do you mean Parent should own data? Thanks. – Alan Jan 14 '22 at 21:34