Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Masha Edelen
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Masha Edelen@mashaedelen )

Updating Reactive Values Can Cause Some Non-Reactive Values To Re-Render In Vue.js 2.5.21

By Ben Nadel on

CAUTION: This post is primarily a note-to-self.

Yesterday, while trouble-shooting some Vue.js code with my esteemed colleague, Daniel Selans, I noticed a behavior that I didn't expect to see: an injected service value was being re-rendered in the component template. This was unexpected because, as the Vue.js document clearly states, the Provide and Inject features don't support reactivity by design. As such, seeing the injected value changes get "tracked" by template gave me pause. I wanted to sit down and experiment with injected-services and template rendering in order to start building out a better mental model for reactivity. After all, this is only day-2 for me and Vue.js.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To test reactivity and service injection, I created a root Vue.js instance that provided a simple service object that contained a single key-value pair:

  • // ... truncated code ....
  • new Vue({
  • el: "my-app",
  •  
  • // I setup the dependency-injection for the descendant components.
  • provide() {
  •  
  • return({
  • service: {
  • value: "Initial value"
  • }
  • });
  •  
  • },
  •  
  • // I render the root component of the application into the DOM.
  • render: ( createElement ) => {
  •  
  • return( createElement( AppComponent ) );
  •  
  • }
  • });

As you can see, this root Vue.js instance is providing an injectable called "service", which contains a "value" property. To test the reactivity of this injectable service, I then created a child component that locates the "service" injectable and renders it within its own template.

I assumed that the initial rendering of the template would work even if there was no reactivity. As such, I wanted to see how the template would update over time. To do this, I initiated an interval inside of the created() life-cycle hook of the child component that starts updating the "value" property every 1,000 ms. At the same time, I also initiated another interval that updated a local "data" value every 4,000 ms:

  • <style scoped src="./app.component.less" />
  •  
  • <template>
  •  
  • <div class="app">
  •  
  • <p>
  • <strong>Computed Service Value:</strong> {{ computedValue }}
  • </p>
  •  
  • <p>
  • <strong>Direct Service Value:</strong> {{ service.value }}
  • </p>
  •  
  • <p>
  • <strong>Shim Value:</strong> {{ shim }}
  • </p>
  •  
  • </div>
  •  
  • </template>
  •  
  • <script>
  •  
  • // Import core classes.
  • import Vue from "vue";
  •  
  • // ------------------------------------------------------------------------------- //
  • // ------------------------------------------------------------------------------- //
  •  
  • export default Vue.extend({
  • inject: [
  • "service"
  • ],
  • data() {
  •  
  • return({
  • shim: 0
  • });
  •  
  • },
  • computed: {
  • computedValue() {
  •  
  • // TEST: Do non-reactive services cause computed values to recompute.
  • return( this.service.value );
  •  
  • }
  • },
  • watch: {
  • "service.value": function( newValue, oldValue ) {
  •  
  • // TEST: Do non-reactive services cause watchers to fire.
  • console.log( "Watcher:", newValue );
  •  
  • }
  • },
  • // I get called once after the component has been created and the props and
  • // dependencies have been wired together.
  • created() {
  •  
  • setInterval(
  • () => {
  •  
  • // TEST: Do non-reactive service updates get rendered.
  • console.log( "Interval:", ( this.service.value = Math.random() ) );
  •  
  • },
  • 1000
  • );
  •  
  • setInterval(
  • () => {
  •  
  • // TEST: Does updating a REACTIVE value cause NON-REACTIVE values in
  • // the same template to re-render.
  • console.log( "Shim-Interval:", this.shim++ );
  •  
  • },
  • 4000
  • );
  •  
  • }
  • });
  •  
  • </script>

As you can see, I am essentially testing three different Vue.js features:

  • How does a computed value interact with a non-reactive dependency?
  • How does template interpolation interact with a non-reactive dependency?
  • How much of the template is re-render when a reactive value changes?

Now, if we run this code in the browser and let the various intervals fire a few times, we get the following output:


 
 
 

 
 Injected services are non-reactive in Vue.js 2.5.21. 
 
 
 

As you can see, when it comes to template interpolation, the computed value never updates. This is because it intelligently caches its output based on changes to reactive dependencies. The directive service value, on the other hand, updates; but, only when a separate, reactive portion of the template also needs to update. In other words, the direct service value interpolation is "swept up" in the change detection caused by the local, reactive "data" property.

We can also see from the console-logging that the "watch" handler for the non-reactive service is never fired.

Based on these behaviors, it's clear that the Vue.js documentation is correct (d'uh). As such, I have to assume that what I was seeing yesterday was a coincidental update of non-reactive interpolation due to temporally-coupled reactive value updates in the same template. My confusion has been mitigated.

ASIDE: This same behavior can also be demonstrated in other front-end frameworks like Angular and React.js For example, in React, component property updates performed outside of .setState() will be re-rendered when other state-based properties are updated.



Reader Comments

Makes sense. Somewhat related - it'd be awesome so see a post digging into reactivity and the various ways you can update values that will or will not trigger updates properly (direct updates of primitives, updates of objects, updates of arrays, Vue.set, all that stuff). It seems like that's something new Vue devs commonly stumble on (at least based on my experience in /r/vuejs). I've been meaning to make a demo of but don't have a lot of time on (and something you can probably do better and reach more folks ;)

Reply to this Comment

@Pyro979,

Yeah, the reactivity model in Vue is really interesting. From what I read, it's all based on creating proxies (in the loose sense of the word) to the underlying data. So, Vue intercepts all mutation calls to "tracked" data. I'm still wrapping my head around it myself (hence this post). I'll let that idea marinate in the back of my mind as I continue to dig into Vue.

Reply to this Comment

Yes. that is because Vue re-renders the component if any of its dependency updates. You could avoid it by creating a sub-component i.e. injected for injected value. In that case Vue will intelligently only update the parent component and not the child i.e. injected component hence giving a performance boost automagically. Thanks

Reply to this Comment

@Nasir,

I like it. This is similar to Angular, I believe. When you move code into a sub-component, it then can be driven by its inputs (aka "props") and stops updating if the inputs remain the same.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.