Avoiding infinite loops while updating an object's properties in Vue JS watchers

I am working with an Array of Objects set up like this:

    let tree = [
    {
       task: "Some Task",
       spentTime : 2, 
       subTasks: {
          task: "Some Sub Task",
          spentTime: 1,
             subTasks:{
                task:"Some sub sub task",
                spentTime:30
             }
       }
    }
 ]

In my project, I am creating a nested accordion to display this type of tree structure. Each node in the accordion has an input box that is linked to the spentTime property using v-model.

If I make changes In any of the input boxes for a node, I need to perform specific operations on the spentTime values and update or add different values within the same object.

My initial thought was to use a deep watch feature. However, I realized this may cause an infinite loop because I would be modifying the same object and assigning new values, which could trigger the watch function repeatedly.

Does anyone have suggestions on how I can execute a function when an input is changed and update the same object with different values? Any help is appreciated!

Answer №1

I encountered similar reactivity challenges while working with Vue.js. It's advisable to utilize Vue.set or this.$set when saving any modifications to your array:

this.$set(this.someObject, 'b', 2)
  • For more insights on Vue set, check out this link.
  • To delve deeper into Vue reactivity, refer to the information available here.

Answer №2

Summary:

For a clean solution, following @djiss suggestion and correctly bubbling up to the top parent using $set and watch, check out this example: https://codesandbox.io/s/peaceful-kilby-yqy9v
The initial answer/logic below uses $emit and the task 'key' to move the update in the parent.


In Vue, modifying the child directly is discouraged. Although possible, Vue warns that such changes could be overridden when the parent changes.

The recommended approach is to manage state with Vuex or a simple Vue object, or inform the parent of the specific child modification required and listen for changes from the parent.

This is demonstrated here:

const task = {
  task: "Some Task",
  spentTime: 2,
  subTasks: [{
    task: "Some Sub Task",
    spentTime: 1,
    subTasks: [{
      task: "Some sub sub task",
      spentTime: 30
    }, {
      task: "Some other sub sub task",
      spentTime: 12
    }]
  }]
};

Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('Task', {
  template: `
  <div>
    <h2>{{task.task}} ({{spentTime}})</h2>
    <div v-if="hasTasks">
      <Task v-for="(t, k) in task.subTasks" :key="k" :task="t" @fromChild="fromChild" :tid="k"/>
    </div>
    <input v-else v-model="localTime" type="number" @input="updateParent(localTime)">
  </div>
  `,
  props: {
    task: {
      type: Object,
      required: true
    },
    tid: {
      type: Number,
      default: 0
    }
  },
  data: () => ({
    localTime: 0
  }),
  mounted() {
    this.updateParent(this.spentTime);
  },
  computed: {
    spentTime() {
      return this.hasTasks ? this.subtasksTotal : this.task.spentTime;
    },
    subtasksTotal() {
      return this.task.subTasks.map(t => t.spentTime).reduce(this.sum, 0)
    },
    hasTasks() {
      return !!(this.task.subTasks && this.task.subTasks.length);
    }
  },
  methods: {
    fromChild(time, task) {
      this.task.subTasks[task].spentTime = time;
      this.updateParent(this.spentTime);
    },
    updateParent(time) {
      this.$emit("fromChild", Number(time), this.tid);
      this.localTime = this.spentTime;
    },
    sum: (a, b) => a + b
  },
  watch: {
    "task.spentTime": function() {
      this.localTime = this.task.spentTime;
    }
  }
});
new Vue({
  el: "#app",
  data: () => ({
    task
  }),
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <Task :task="task" :tid="0" />
</div>

This component can handle any tree structure given to it, as long as it follows the same format. The logic is to display an input if there are no subtasks or calculate from subtasks otherwise.
Feel free to customize according to your requirements.

Answer №3

I encountered a similar issue in the past, and I managed to overcome it by utilizing deep watch along with Lodash functions like _.cloneDeep and _.isEqual.

Within your child component, establish your own data named componentTask. Keep a close eye on changes within both componentTask and your prop, comparing them using _.isEqual. Whenever there is a change in componentTask, trigger an event up to its parent.

SubTask:

<template>
    <div>
        <input type="text" v-model="componentTask.task">
        <input type="number" min="0" v-model.number="componentTask.spentTime">
        <SubTask v-if="task.subTasks" @task-change="handleTaskChange" :task="task.subTasks" />
    </div>
</template>

<script lang="ts">
    import {Vue, Component, Prop, Watch} from 'vue-property-decorator'
    import {Task} from "@/components/Test/Test";
    import _ from "lodash";

    @Component
    export default class SubTask extends Vue {
        @Prop() task!: Task;

        componentTask: Task | undefined = this.task;

        @Watch('task', {deep: true, immediate: true})
        onTaskChange(val: Task, oldVal: Task) {
            if (_.isEqual(this.componentTask, val))
                return;

            this.componentTask = _.cloneDeep(val);
        }

        @Watch('componentTask', {deep: true, immediate: true})
        onComponentTaskChange(val: Task, oldVal: Task) {
            if (_.isEqual(val, this.task))
                return;

            this.$emit("task-change");
        }

        handleTaskChange(subTasks: Task){
            this.componentTask = subTasks;
        }
    }
</script>

Parent class:

<template>
    <div style="margin-top: 400px">
        <h1>Parent Task</h1>
        <br>
        <div style="display: flex;">
            <div style="width: 200px">
                <h4>task</h4>
                <p>{{task.task}}</p>
                <p>{{task.spentTime}}</p>
                <br>
            </div>
            <div style="width: 200px">
                <h4>task.subTasks</h4>
                <p>{{task.subTasks.task}}</p>
                <p>{{task.subTasks.spentTime}}</p>
                <br>
            </div>
            <div style="width: 200px">
                <h4>task.subTasks.subTasks</h4>
                <p>{{task.subTasks.subTasks.task}}</p>
                <p>{{task.subTasks.subTasks.spentTime}}</p>
                <br>
            </div>
        </div>
            <SubTask :task="task" @task-change="handleTaskChange"/>
    </div>
</template>

<script lang="ts">
    import {Vue, Component, Prop} from 'vue-property-decorator'
    import SubTask from "@/components/Test/SubTask.vue";
    import {defaultTask, Task} from "@/components/Test/Test";

    @Component({
        components: {SubTask}
    })
    export default class Test extends Vue {
        task: Task = defaultTask;

        handleTaskChange(task: Task) {
            this.task = task;
        }
    }

</script>

Defined interface:

export interface Task {
    task: string;
    spentTime: number;
    subTasks?: Task;
}

export const defaultTask: Task = {
    task: "Some Task",
    spentTime : 2,
    subTasks: {
        task: "Some Sub Task",
        spentTime: 1,
        subTasks:{
            task:"Some sub sub task",
            spentTime:30
        }
    }
};

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

"When the value is false, use the Vue binding to apply a specific

My challenge involves managing a website that is designed to receive boolean values and store them in an array where the key represents the ID and the value represents the boolean. For example: policiesActive[ "2" => false, "3" => false] The w ...

Ways to consistently stream information from server to browser

I have a file that is constantly being updated with the following contents: id,value,location 1234,pass,/temp/... 234,fail,/temp/r/... 2343,pass,/temp/status/... The file keeps updating for approximately 1 hour by a program. I want to display ...

Why is React JS unable to discover my exported modules?

Upon running my React app, the console displayed the following error message: Failed to compile. ./src/components/login/index.js Attempted import error: 'Login' is not exported from './login'. Here is an overview of the folder struct ...

There seems to be a column in the MySQL INSERT query that is unidentifiable and does not exist in the list of fields

id and cost are required as integers and cannot be left empty. I encountered the following error message: 'Error: ER_BAD_FIELD_ERROR: Unknown column 'undefined' in 'field list'' var sql = "INSERT INTO Paintings(` ...

How can I use the v-btn 'to' prop to redirect to a different website instead of just adding a path to the base URL (localhost)?

I've set up a Vue.js project with Nuxt and Vuetify.js. I'm attempting to incorporate FontAwesome icons as v-btns to direct users to an Instagram page. However, when clicking on the icon, the browser redirects to http://localhost:3000/https://ww ...

The CORS Policy in a Google Cloud Platform App Engine is causing interference with my API requests on a live website

I successfully deployed my node.js server to the Google Cloud App Engine service in order to make my frontend work. After uploading the frontend, my website is now live. The node.js server is running smoothly and deployed on the App Engine within Google C ...

Is it possible to simultaneously run multiple functions with event listeners on a canvas?

I'm attempting to create a canvas function that displays the real-time mouse cursor location within the canvas and, upon clicking, should draw a circle. I came across this code snippet that reveals the x and y coordinates of the mouse: document.addEve ...

Accessing a Child Component Function in Material UI

Utilizing the Material UI framework, I am constructing an application with React. Included in this application is a Dialog feature that permits users to modify various information about an object (referencing the code below). The dialog consists of two but ...

Eliminating bottom section in HTML/CSS

I've got this code snippet: new WOW().init(); /* AUTHOR LINK */ $('.about-me-img img, .authorWindowWrapper').hover(function() { $('.authorWindowWrapper').stop().fadeIn('fast').find('p').addClass('tr ...

Activate a single button to reveal hidden buttons

My HTML page is filled with numerous buttons, each designed to expand specific information upon clicking. To enhance usability, I am seeking a way to use one main button to expand all the others. Initially, I attempted to display only one button alongsid ...

What is wrong with my notecards that they won't flip properly?

I am currently developing a text-to-flashcard program using HTML, CSS, and JS. One feature I'm working on is the ability to flip all flashcards at once with a single button click. Currently, the program only allows flipping from the back face to the f ...

Is it possible to adjust the timezone settings on my GraphQL timestamp data?

I've come across a lot of helpful information regarding the usage of Date() and timezones, but something seems to be off. In my GraphQL setup (sourcing from Sanity), I have configured it to use formatString in this manner: export default function Minu ...

Updating information within AngularJS select boxes

On my page, I have 3 select boxes. When a user selects an option in the first select box, I want the options in the second select box to update based on the value selected in the first one. Similarly, I want the options in the third select box to change w ...

Bringing the value from the codebehind to the jquery function on the client side

Here is the code snippet from my asp.net web application's code behind where I have integrated another web service: [WebMethod] public static string GetData(string name) { WEBSERVICE.Service1 Client = new Service1(); string Nam ...

Implementing text truncation in JavaScript

I am seeking to transform the INPUT I have into a desired OUTPUT format. pieChart.js stackedColumnChart.js table.js and i want OUTPUT like that(wanna remove .js from ) pieChart stackedColumnChart table ...

Storing external API requests in create-react-app's service worker for faster retrieval

I'm in the process of transforming a React web application into a PWA (Progressive Web App). I've made the necessary change in the index.js file - serviceWorker.register();. Everything is functioning properly as I can access the home page and as ...

Solution for fixing the error: MongooseError [OverwriteModelError]: It is not possible to overwrite the `User` model after it has been compiled in

I am new to working with the MERN stack and currently attempting to create an exercise tracker app following a tutorial on YouTube. However, I am encountering the Mongoose: OverwriteModelError when running the server and cannot seem to identify where I am ...

Bi-weekly Calendar Gathering

I am able to get my events sorted by day with the following code: daysOfWeek: [2], However, I have not been able to find any information in the documentation on how to make it sort fortnightly. Can this be done with fullcalendar? Solution: Just a heads ...

Merging an AppBar and Drawer in Material UI for a seamless user interface design

I am working on integrating an AppBar component with a drawer feature. Here is the code for the AppBar: import React from "react"; import PropTypes from "prop-types"; import { withStyles } from "material-ui/styles"; import AppBar from "material-ui/AppBar" ...

Tips for incorporating several form input outputs into an array State in React

I am an aspiring coder diving into the world of React by working on a simple app that helps calculate how much money one has, designed for my kids to use and learn from. The app consists of 5 components, each with its own input field. These fields allow us ...