Forcing Re-computation of Vue's computed properties

11 Oct, 2019

Recently I wanted a computed property to re-compute its value because it has a non-reactive dependency so it needed a tiny spark to start up. Here I showcase a little trick you can use to force reactivity system into re-computing the values on demand.

The problem

Just recently I was working on implementing some reducers for vee-validate, currently it is a draft but I was faced with the problem of making computed properties based on some $refs and non-reactive arbitrary data.

Specifically I needed to create a computed prop that will return a temporary value that is non-reactive until some stuff has happened. Once it happens it will instead return the actual value which is reactive and the computed prop will start working as intended.

A good example of this is that you have a computed prop based on some custom injected properties. Because they might be null we would need to return a "temporary" value to act as a safety net for our templates and JavaScript.

Here is a small example that introduces a temp value until the element is mounted:

<template>
  <div>
    <input ref="element" @input="value = $event.target.value">
    Uppercased is: {{ uppercase }}
  </div>
</template>

<script>
export default {
  name: "Demo",
  data: () => ({
    value: ""
  }),
  computed: {
    uppercase() {
      if (!this.$refs.element) {
        return this.$blankVal;
      }

      return this.value.toUpperCase();
    }
  },
  created() {
    this.$blankVal = "I will never update!";
  }
};
</script>

Of course the example is impractical and will probably never happen to you. But for simplicity sake let's pretend that's an actual use-case.

Basically we've written ourselves into a corner. Vue registers reactive dependencies upon access, but we never access anything that's reactive, thus no dependencies are collected and effectively we short circuited our computed prop.

Here is a running example:

How Vue Reacts

As of this writing, Vue 2.x uses Object.defineProperty to convert our state like data into reactive values. That is wrapping them in ES5's getters and setters. Once any of those properties are accessed, they trigger the get handler allowing Vue to register whatever that accessed this prop as a dependency.

And easily once it is assigned a value, the set handler triggers. Allowing Vue to notify those dependencies about what just happened.

Here is a few useful links in depth about that:

Now in our example our $blankValue is never defined in props or data or computed meaning it is not reactive and will never be. Here is a breakdown of how the reactivity system did its thing:

  1. Create Component Instance, assign data props and make them reactive.
  2. Render the template, encountered uppercase reference to computed prop.
  3. Get the uppercase prop value, the condition true, return value.

No reactive dependencies were collected, during the execution path because of our condition, we've short circuited our computed prop and made it static.

Exploring Solutions

A famous silver bullet is a method called $forceUpdate which is is available on all components and forces a re-render on the component that called it. Notice that it doesn't help us at all.

Because computed props are cached, the value is kept as is and since no reactive dependencies changed, Vue sees no reason to re-evaluate our computed value. We are still stuck!

We could also disable computed properties cache but that would mean that we went full circle and effectively written a data with a watcher, hardly useful.

Hacking the reactivity

Now that we understand more about computed properties and reactivity in Vue, let us state our goals. We want to be able to:

  • Force a computed prop to re-evaluate.
  • Re-evaluation should only affect this prop and its dependencies.
  • Control the re-evaluation on demand.

To approach this problem, let's think of ourselves as hackers. If you watched Mr. Robot this should be fun.

To hack a system, we need to have an inside-man, a back door that we can use to gain control over the system. In our terms, we need to add something to our computed prop. Maybe a reactive prop that we can control.

export default {
  data: () => ({
    value: "",
    backdoor: 0
  }),
  computed: {
    uppercase() {
      this.backdoor; // just referencing it is enough!
      if (!this.$refs.element) {
        return this.$blankVal;
      }

      return this.value.toUpperCase();
    }
  },
  created() {
    this.$blankVal = "I may never update!";
  },
  mounted() {
    this.backdoor++;
  }
};

Here it is in action:

So it works, whenever we want the computed prop to re-evalute we will just increment backdoor and it will force our component to re-compute it!

Let's take things a little step further, lets not use value at all and use $refs.element.value itself. That means our computed prop is using only non-reactive dependencies, to make it reactive we will need to watch value and increment backdoor to force it to re-evalute.

export default {
  data: () => ({
    value: "",
    backdoor: 0
  }),
  computed: {
    uppercase() {
      this.backdoor;
      if (!this.$refs.element) {
        return this.$blankVal;
      }

      return this.$refs.element.value.toUpperCase();
    }
  },
  created() {
    this.$blankVal = "I will never update!";
  },
  watch: {
    value() {
      this.backdoor++;
    }
  },
  mounted() {
    this.backdoor++;
  }
};

And that's it, we have complete control over when this prop is re-computed and take advantage of having a computed cache as well. This can be useful for complex computed values.

Refactoring

Now let's make things a little bit more re-usable, it will be counter intuitive to keep adding backdoor values to each and every component that we need to have its computed value re-computable.

Let's make a computed prop factory:

function recomputable (fn) {
  // TODO: Make it re-computable.
  return fn;
}

The recomputable function is just a higher order function that wraps our computed function and returns them, hopefully after modifying something about their behavior.

We have quite a few stuff to do, we need to add our backdoor to the computed prop and allow our component to trigger re-evaluation of that prop. But note that we don't want to use mixins to add our backdoor, we want it to be isolated within our recomputable function.

We can use Vue.observable that was introduced in Vue 2.6, it will allow us to create reactive values without having to use components or mixins.

function recomputable (fn) {
  const reactive = Vue.observable({
    backdoor: 0
  });

  return function () {
    reactive.backdoor; // reference it!

    return fn();
  };
}

This makes our prop re-computable but we have no way of triggering increments to our backdoor value, we did want it isolated after all.

For my use-case with vee-validate I only needed to re-compute once the component is mounted, so I did something like this:

function recomputable (fn) {
  const reactive = Vue.observable({
    backdoor: 0
  });

  return function () {
    // Will only execute once, never again!
    if (!reactive.backdoor) {
      this.$on('hook:mounted', () => reactive.backdoor++);
    }

    // We already referenced it!
    // reactive.backdoor;

    return fn();
  };
}

Let's go for 100% and add a way to trigger re-computations, ideally we want to have a recompute function that we can use like this:

// recompute `propName` on the current component.
recompute(this, 'propName');

We can start writing our recompute function like this:

function recompute (vm, propName) {
  // TODO: INCREMENT BACKDOOR.
}

Sadly we have no way to access the reactive backdoor value from inside recompute. We will need to add some custom injections to get it to work, let's modify our recomputable

function recomputable (fn, name) {
  const reactive = Vue.observable({
    backdoor: 0
  });

  return function () {
    // initialize a map once.
    if (!this.$__recomputables) {
      this.$__recomputables = {};
    }

    // add a reference to my reactive backdoor trigger.
    if (!this.$__recomputables[fn.name || name]) {
      this.$__recomputables[fn.name || name] = reactive;
    }

    // Same as before ...
  };
}

We needed name to be able to add as a key to our $__recomputables map that will hold references to our values, I try to offer a reasonable fallback if the user provided a named function. We can now trigger re-computation on demand:

function recompute (vm, propName) {
  // handle non-existent props.
  if (!vm.$__recomputables || !vm.$__recomputables[propName]) {
    return;
  }

  vm.$__recomputables[propName].backdoor++;
}

Then we can finally use them like this:

import { recompute, recomputable } from "@/recompute";

export default {
  name: "Demo",
  data: () => ({
    value: ""
  }),
  computed: {
    uppercase: recomputable(function uppercase() {
      if (!this.$refs.element) {
        return "I will get recomputed :(";
      }

      return this.value.toUpperCase();
    })
  },
  mounted() {
    recompute(this, "uppercase");
  }
};

And that's it, we are done. Let's verify our solution is working in this sandbox:

Great, once the component is mounted the computed value is re-computed and will be reactive since we defer to a reactive execution path after the initial conditions.

How useful is this?

Honestly, this isn't really useful in production apps. But can be really useful for library authors and library maintainers as they usually try to do more stuff under the hood to keep things clean and isolated.

I would say if you don't need this, then you are doing a very good job not to write yourself in a corner like that. Well Done!

Conclusion

We learned some stuff about computed values, and we managed to fine tune their cache and reactivity behavior. Thanks for reading!

Liked it? Subscribe for my latest content!