Search code examples
javascriptvue.jsvuejs2mixins

You may have an infinite update loop in a component render function, caused by side effect in method


I've solved my issue, but I don't understand it (or maybe I do).

I created an error handler plugin, basic example:

main.js

const bugStore = {
  state: [],
  addToState(bug) {
    this.state.push(bug);
  }
};

const ErrorLoggerPlugin = {
  install(Vue) {
    const app = new Vue({
      data: {
        bugStore: bugStore
      },
      methods: {
        bug(bug) {
          // Necessary to prevent re-rendering of components
          //setTimeout(() => {
          this.bugStore.addToState(bug);
          //});
        }
      }
    });
    Vue.prototype.$bug = app.bug;
    
  }
};

Vue.use(ErrorLoggerPlugin);

new Vue({
  el: "#app",
  components: {
    App
  },
  template: "<App/>",
  render: (h) => h(App)
});

App.vue

<template>
  <div>
    <img alt="Vue logo" src="./assets/logo.png" />
    {{ $injector("hello world") }}
  </div>
</template>

<script>
export default {
  name: "App",
  components: {},
  methods: {
    $injector(val) {
      try {
        breakme();
      } catch {
        this.$bug("anything");
      }
      return val;
    },
  },
};
</script>

By doing this, I end up with an infinite loop in the render function of App.vue

I won't be using a method in a template. I know to avoid this. But I'm trying to understand why this causes the component to re-render.

If you uncomment the setTimeout in main.js it prevents the issue.

Understanding setTimeout, this allows the method to return the value before further execution of the bug handler plugin is performed.

After a good nights rest, I think I have an idea of what's going on. But this also begs the question, what would be a better way to handle this?

What's happening:

I think that when the template uses this method call it creates notification dependencies within the stack of the method, and therefore the component becomes reactive to the state of the plugins store. And if I wrap it in a setTimeout, that related dependency isn't executed until the current stack clears.

https://codesandbox.io/s/modest-babbage-cjuw71?file=/src/main.js

So I suppose I need to re-think how I'm handling this. I have a global error handler that doesn't cause issues, but there are times that I want to handle an issue intentionally and report this to the notification engine. In fact, it may not even be an error. This can be anything.

UPDATE

I created a functional component to replace the functionality of what was previously seen done with a method. Same issue.

UPDATE

I removed the new Vue from the plugin as suggested by this answer: https://stackoverflow.com/a/75625507/1447679

But in my case I needed reactivity in another portion of the app. That component, I referenced the state of the bug store in its own data attributes to make it reactive, and everything worked out.

TAKEAWAY

Anything that's part of the rendering of a component, unless otherwise specified with v-once, will create a notification/dependency tree from anything it touches. So if the method changes the state, even seemingly not related/tied to the particular component, it will react to this change and re-render the component.


Solution

  • I found out the problem was that you create a new Vue app in your plugin.
    This is causing a kind of circular reactive dependency between your main app, and the plugin one, which provoke the infinite render loop.

    If you simply avoid creating this internal Vue app (because you don't need it), the problem is gone:

    const bugStore = {
      state: [],
      addToState(bug) {
        this.state.push(bug);
      }
    };
    
    const ErrorLoggerPlugin = {
      install(Vue) {
        Vue.prototype.$bug = (bug) => {
          bugStore.addToState(bug);
          console.log("bug added");
        }
      }
    };