Practical Fullstack approach to Vue3

I am a fullstack developer. I do user support, requirement analysis, nginx configuration, backup handling, integrations and css. Much of my work is centered around Node.js and Vue.

I do not love Vue and I am not fascinated with it, nor interested in its internal working. It is a tool to write reactive (2-way-binding) web applications and to modularize web code (write small reusable components).

I started using AngularJS (v1) years ago. That changed the way I thought of web development and I became more productive making better web applications. However AngularJS is kind of abandoned now, and it is a much heavier framework than need. So I started transitioning to Vue2 some years ago thinking of it as AngularJS-light. In most ways Vue2 is nicer than AngularJS, except sometimes it (the reactivity system) does not work (predictably). Well, Vue2 probably works exactly as it is written to work, but for me as a developer I think I do what works, and then it fails for some delicate detail that I can figure out if I have time, maybe. That is waste of time and it adds uncertainty to my job.

I have this feeling with Vue2 that I need a plan (a design pattern) and I still have not found one that works well. Now I started migrating Vue2 to Vue3 and things change a bit (and things break a bit). So now I really want to find simple design principles for my Vue3 applications, that I can follow so everything works without me thinking about it.

State

The real challenge when writing an (single page web) application is managing the state:

  • Information about session
  • I/O and error handling
  • Incoming data, updating state and UI, avoiding conflicts
  • Application settings, like filters and user choices
  • Data validation and invalid-data-feedback to user
  • Data modelling

This is harder than writing a good user interface. So the client-side-design needs to be state-first, not UI-first. Also

  1. I am migrating things from Vue2 to Vue3 that were originally AngularJS. If the state, business logic, I/O, were dependent on AngularJS-stuff I would be stuck in AngularJS.
  2. I can write this state in generic JS (not specific for web/UI) and thus make it possible to also run it on a server, exposing it as APIs and do automated testing

So I am not interested in Veux. Perhaps it can run backend, I do not care. This is how I think of my architecture:

  1. GUI (as thin layer as possible)
  2. Web component business logic (plain JS, no Vue, can run in Node.js)
  3. State (plain JS, no Vue, can run in Node.js)
  4. Server Side (Node.js)

Most code can (ideally) run in 2-4, and can thus be tested in Node.js. Vue is only about (1).

State – Vue – Interface

Some applications are large and state consists of many modules. Some are small. For a simple example I think of state as (this is, admittedly based on Vue2-experience, where you need to consume state data in a way that makes it reactive in Vue).

STATE = {
  ro : { ... things that are readonly to Vue (like server data) ... }
  rw : { ... things that Vue can change (like filter choices or user input) ... }
  api: { ... functions that can do stuff with state ...}
}

Typically I would get this from a plain JS factory function where I can supply dependencies that are possibly implemented (for I/O) differently on Browser and Node.js:

function MyStateFactory(libIO, lib1, lib2) {
const state = { ro:{} , rw:{} , api:{} };
... set up state ...
return state;
}

How do I make this state available (and reactive) in my Vue application and Vue components? I can admit that in the past I have been lazy and done things like (pseudo code):

Vue.Component({
  data : function() {
    return { state : STATE };
  },
  ... more stuff
});

That kind of works in many cases! But I have also ran into problems. In a real application you have 3 types of components:

  1. Those that depend only on supplied props
  2. Those that depend only on state
  3. Those that depend on a mix of props and state

In theory, all components should be (1) and all other design is rotten (you may think). In practice 2 and 3 are useful too.

So the real questions are:

  1. How do you expose state, as data and functions, so changes can be picked up by the right components?
  2. How do you design components so they update when they should?

AngularJS – Vue2 – Vue3

AngularJS has a function that recalculates everything, over and over again until it gets the same result twice. Then it updates the DOM accordingly. AngularJS does not detect changes to data automatically, however when you use its api to manipulate data, you are also telling AngularJS that you are changing data, and the function mentioned before can run. If you change data from outside angular you may need to use $scope.$apply() to tell Angular to refresh.

In Vue2, when you make an object part of “data” in a component, it becomes “reactive”. This is very implicit and it has some limitations (with Arrays, for example). What if a prop changes? What if you have a method that uses external data (as a state outside Vue), does that trigger refresh of the component? If a parent refreshes does that refresh the children? What if a child element deep down in data/prop changes? What if properties in data are replaced/added rather than modified? All I say is that to someone so stupid and ignorant as me it is not obvous how to make Vue2 just work.

Vue3 is more explicit (I interpret that as Vue2 was unpredictable not only for me, and that the Vue2 abstraction was leaky). While AngularJS and Vue2 hide the mechanism that drives reactivity Vue3 has the very explicit functions:

  • ref
  • reactive
  • readonly
  • shallowRef
  • shallowReactive
  • shallowReadonly

These functions are what connects the Vue controllers to the outside-of-Vue-world, and makes it possible for Vue to detect changes.

Vue3 – a first simple practical design

So, now we can think that we have something like this:

var state = { a:1 };
state = Vue.reactive(state);

Vue.component({
  props : { /* parameters to component here */ },
  data : { /* component local data here */ },
  methods : { /* access state via functions here */ }
});

Lets explore what happens and what works!

Vue.reactive()

We need to understand – exactly – the difference between the argument and the result of Vue.reactive():

var state_reactive = Vue.Reactive(state_original)

I have experimented (test0) and found that:

  • They share the same data, that is, the data lives in state_original
  • If you add/delete/modify data in one of them, it is immediately available in the other
  • state_orginal, and none of its children, will ever be reactive
  • state_reactive, and all of its children, will always (more on this later) be reactive
  • state_orginal and state_reactive (and all of its corresponding children) are never the same object (everything in state_reactive is Proxy to state_orginal)

The consequence of this is that your state-library must modify data in state_reactive, if you want Vue to be notified of the change (when Vue anyway refreshes for any reason, it will get the changes).

So the problem becomes:

var state = StateFactory();

// will not work because the internal state of StateFactory is not affected
state = Vue.reactive(state)

// this has severe implications:
//  1) Vue is modifying something inside the state library
//     (that was explicitely supposed to be read-only)
//  2) How does the state library protect itself?
//     possibly keeping an internal ro, and replacing the state.ro regularly
state.ro = Vue.Reactive(state.ro)

So I would say that you will need to design your state library with this in mind.

  1. Be clear that state.ro, state.ro can be made reactive this way, or
  2. Expose something else that can be made reactive (state.updated), or
  3. If the “Vue” object is present, the library could use it and take care of making the right things reactive itself, or
  4. The library has some way to notify of updates (an update callback function), and in the Vue-world, you let that callback be a function that updates something that IS reactive already.

Regardless of this little problem it is a much better situation than the Vue2-way of adding external objects in data and hoping for the best.

What Vue3 Components are updated?

Now that we know what data outside of the component that is reactive, the questions are:

  • what changes trigger the component to be updated?
  • when do child components update?
  • can any number of components, independently, be triggered by the same reactive variable?
  • if a reactive variable is used in a method, does that trigger an update
    • even if the method is not always called
    • even if the variable is not always used in the method (only some branches)
    • even if the result of the method does not change
    • even if the result of the method is not rendered
    • when the method called on any other occation (like polling)
  • is there any risk that some components are updated multiple times (in one cycle)?
  • in what order are the components updated, and can it matter?
  • what is enough to trick/force a component to update?
  • can a component update only partly?

I do not really want to know. I just want a way of designing my components so they work predictably. What I have found in the past is that when I write one big Vue-component (or application) I have no problems. But when I start modularizing it, with components that are sometimes but not always nested, that is when problems start. I have asked myself questions (with Vue2) like:

  • can one component override/replace what triggers another component, making the earlier “trigger-subscriber” dead?
  • can I end up with deadlocks, infinite recursion or mutual dependencies?
    (it has happened)

We kind of can not keep all these details in mind when building an application. We need to have a simple design that just works every time.

Single Application

I have written a simple test (test1) application with no child components. I want to explore how little reactivity works.

// Start with a STATE:
  STATE = { lines: [] };

  setInterval(() => {
    updateLines(STATE);  // implemented elsewhere somehow
  }, 2000);

// Make reactive
  STATE.lines = Vue.reactive(STATE.lines);

// Make a simple app (not showing everything)
  Vue.createApp({
    data: {
      function() {
        return { lines : STATE.lines }
      }
    }
  });

// In HTML Template, do something like
  <tr v-for="l in lines">...</tr>

This works (as expected) IF updateLines does not replace the lines array. So I have experimented and found:

Vue.reactive(STATE.lines)updateLines keeps linesworks
Vue.reactive(STATE.lines)updateLines replaces linesweird behaviour
Vue.reactive(STATE)updateLines keeps linesworks
Vue.reactive(STATE)updateLines replaces linesweird behaviour
Vue.shallowReactive(STATE)updateLines keeps linesnot at all
Vue.shallowReactive(STATE)updateLines replaces linesnot at all

The problem here is that when STATE.lines becomes a new array, the data:function in createApp does not re-run, so we keep track of and old array that STATE no longer cares about (the weird behaviour is that updateLines kept part of the old lines-structure and that “garbage” is still reactive).

It is clearly a sub-optimal situation that the implementation of state, and not just what STATE looks like, matters. 4 alternatives do not work, 2 work but are bad design. What about:

  STATE = Vue.shallowReactive(STATE);

// Make a simple app (not showing everything)
  Vue.createApp({
    data: {
      function() {
        return { state : STATE }
      }
    }
  });

// In HTML Template, do something like
  <tr v-for="l in state.lines">...</tr>

This works also sometimes:

Vue.shallowReactive(state)updateLines keeps linesnot at all
Vue.shallowReactive(state)updateLines replaces linesworks
Vue.reactive(state)updateLines keeps linesworks
Vue.reactive(state)updateLines replaces linesworks

The only thing that works regardless how updateLines is implemented, is to make all of STATE recursively reactive and make all of it data in every component. Exactly what I admitted above that I had been doing with Vue2.

shallowReactive is appealing, but it depends on the inner implementation of state, and that kind of design will quite possibly give you nasty bugs later when something changes.

So, making your data embrace STATE, or part of states works only if you know how state updates itself, unless you make exactly all of STATE recursively reactive. I think more people than I find that sledgehammer approach unsatisfying.

How about state signalling that something is updated, by updating a primitive variable (value, to avoid using ref and then ending upp with value anyway)? Note that updated.value is truthy from the beginning, and it will alwasy be truthy (++ only operation on it ever), but Vue does not know that so it needs to read and check.

  STATE = { updated: {value:1} , lines: [] };

// Make just updated reactive
  STATE.updated = Vue.reactive(STATE.updated);

  setInterval(() => {
    updateLines(STATE);
    STATE.updated.value++;
  }, 2000);

// And in Vue.createApp
  data : function() {
           return {
             lines : STATE.lines,
             updated : STATE.updated
           }
         }

Now, we can not rely on

  • lines, because it is not reactive at all
  • updated in data because it is not used in the template

However, there are some things that work. First it seems nice to replace lines in data with a method:

  methods : {
    getLines : () => { return STATE.lines; }
  }

  <tr v-for="l in getLines()">...<tr>

However, that is not enough because lines is still not reactive. But here are 3 simple little hacks that do work (and of course there are or more):

  <-- output updated in the template - not typically wanted -->
  Updated: {{ updated.value }}

  <-- display table (parent of lines) "conditionally" (always truthy)-->
  <table v-if="updated.value">

  // use updated.value in getLines()-function
  getLines() => { return STATE.updated.value ? STATE.lines : []; }
  getLines() => { console.log(STATE.updated.value); return STATE.lines; }
  // assuming devnull exists and does nothing
  getLines() => { devnull(STATE.updated.value); return STATE.lines; }

You can use this.updated.value if you use function() instead of ()=>{}. I find it quite a positive surprise that the above works even if neither lines nor updated is in data:

  STATE.updated = Vue.reactive(STATE.updated);

  data: {}
  methods: {
    getLines : () => { return STATE.lines; }
  }

  <table v-if="updated()">
    <tr v-for="l in getLines()">...</tr>
  </table>

This is beginning to look like something that appeals to my sense of simplicity. The conclusion for simple application, external state, and no components is that you have two (simple to explain) options:

  1. Make all of STATE (the exposed data, not functions) recursively reactive. Use it directly from everywhere.
  2. Make only one variable, STATE.updated, reactive. Make sure to poke (++ is probably ok) that variable whenever you want anything else to update.

Beware of doing things like value = STATE.some.child.value for anything that is expected to work beyond the next “tick”. Please note that I have not build a big single-application-zero-components application this way. So for now, this is just a qualified hypothesys.

You can check out the final result as test1.

Application with components

I split my application into three components. The application itself has no data or methods.

  • Application (test2)
    • Market
    • Portfolio
    • Copyrigh notice (nothing reactive, should fail to update)

This worked fine with no changes to reactivity compared to test1. So I got confident and made components for the lines:

  • Application (test3)
    • Market
      • Market-quote
    • Portfolio
      • Portfolio-stock

This did not immediately work. The template in Market had this content:

  <test-market-quote v-for="q in getQuotes()" :q="q">

getQuotes is still depending on update.value and is called every time, but it “happens” to return the same, modified, quote-lines every time. So the test-market-quote does not realise anything changed:

  APP.component('test-market-quote',{
    props : {
      q : Object
    },
    data : function() { return {}; },
    template: '#test-market-quote',
    ...

So I needed to replace (in a method in the test-market-quote component):

  return API.valueOf(stock) < STATE.trader.cash;
//with
  return STATE.updated.value && API.valueOf(stock) < STATE.trader.cash;

in order to make sure some method in the child component also is dependent on updated.value. This was not needed when there were no child components, becuase the parent component had another dependency on updated.value and that cause the update of the entire component (but obviously not forcing its children to update). That worked in one component, but the other component had no methods to add a dependency to, so I successfully replaced

  <td>{{ s.name }}</td>
<!-- with -->
  <td>{{ upd(s.name) }}</td>

//and added
  methods : {
    upd : (v) => { return STATE.updated.value ? v : 0; }
  }

Reality check!

This is beginning to be ridiculous. How many obscure hacks are we going to use to not have to rely on the reactivity system that is at the heart of Vue? These kind of hacks are also a source of (subtle) bugs and can make refactoring harder. The problem I have experienced with Vue is that I need to understand how things work under the hood. But with these different updated.value hacks the cure is getting as bad as the disease (that said, these hacks are probably anyway things you need to do, if components do not update when you want them to).

So I was thinking about a universal fix for updated.value (test4):

// first a reusable function
  API.yes = () => { return !!updated.value; }

// second every (child) component makes it part of its methods
  methods: {
    yes : API.yes
  }

// third, every component template has a root element, add a v-if
  <div v-if="yes()">
    .. component template body ..
  </div>

This works! It is rather simple.

Two working practial design choices

So, we have arrived at two rather simple (conceptually) ways to build predictable Vue3 applications that rely on external (Vue-independent) state code:

  1. Make all state recursively reactive, from the root
  2. Only make an update-variable reactive, and make all components depend on it

Obviously there does not have to be exactly ONE state. There can be some log-state, some io-state, some user-setting-state, some data-state and they could possibly work differently.

Regardless, it is important to understand that if there is an exposed state, the code responsible for the state may update objects, replace object, reuse objects and move objects around in the state (tree), unless the state is very clear and strict about this.

How is the recursive reactivity-tree updated?

We have a plain object:

  x1.a.b.c.d = 5;

// make it recursively reactive

  x2 = Vue.reactive(x1); 

// we add something in x1

  x1.a.b.c2: {d : 3.14} };

// NOW c2 and c2.d can not possibly be reactive, because if
// Vue could know that we did that to a child of x1, we would
// not need to make a reactive copy (proxy) of x1 in the first
// place. The above operation does not trigger anything in Vue.

// How does it happen that c2 and c2.d *eventually* end up
// begin reactive?, that is
//   isReactive(x2.a.b.c2.d) => true

// Well I think when you do that (isReactive) you are accessing
// x2, x2.a, x2.a.b and eventually x2.a.b.c2. That means, you
// are asking the Proxy b to give you its child c2 - which it was
// not aware of. But now it is!
// So it can find c2, make it recursively reactive before giving it
// to you so you can resolve x2.a.b.c2.d and pass it to isReactive().

That is how I think it works.

Vue2 vs Vue3

I really have not code very much in Vue3 yet. But I have done enough research to write this post. I think Vue3 seems to be MUCH better.

In Vue2 I ended up doing:

STATE = { ro:{}, rw:{}, api:{} };

Component({
  data : function() {
    state_ro : STATE.ro
  },
  methods : {
    stuff : function(
      state_ro.foo ...     // is there any difference?
      ... STATE.ro.bar     // does it matter if I use
    )                      // state_ro or STATE.ro?
  }

This mess is GONE with Vue3. Making things reactive is 100% explicit. data can be used exclusively for local variables to that component instance. methods can be used to get external state data, and if it is reactive that methods will trigger and the component will update.

I can imagine that this can essentially be done with Vue2 as well. But with the implicit reactivity system based on putting external state in data I never managed to figure it out.

When my code is migrated to Vue3 I will never look back at Vue2. I think.

Performance

I get a bad feeling when I think of thousands of lines, where each line is a complex object (like an invoice), and everything is made reactive, recursively. And that there are thousands of components on my web page, that each depend on several objects/properties in every invoice. And that these thousands of lines may just be discarded when something new arrives over the network, garbage collector trying to make sense of it, and new complex lines comes in to be made reactive. Perhaps it takes 0.05s and it is no problem. Perhaps it always works.

To just render everything again is usually no problem, very predictable, and rather easy to optimize if needed.

But I think like I did with AngularJS. Use the reactivity-system inside the components. But do not let the components, or the Vue Application for that matter, be made aware of everything. Give to it what it needs to do, to render everything and to allow for user input/edit where required.

A second thought…

I wrote a minesweeper game (test5) with the intention of abusing the reactivity system and see if performance got bad. It turned out I wrote a too good implementation, and the reactivity system worked perfectly. So I failed to prove my idea, but I learnt something on the way: that the reactivity system is better than I first thought – not the implementation in Vue but the idea, fundamentally.

My fear was that I essentially have two trees of objects

  • Data, nested data, deeply nested data, complicated data and separated data
  • UI, nested components, deeply nested components and separate components

My fear was that data consists of tens of thousands of reactive nodes, and each of them will trigger an update to hundreds or thousands of UI-components. However…

…in reality the data-tree will much resemble the ui-component-tree. Often there will be 1-to-1 relationships. This means that nothing will be updated in vain, and everything (and only that) which should be updated will be updated, typically each triggered by one or a few reactive “events”.

What would be wasteful?

  • If a single small component depends on a lot of spread out data – but that is already wasteful design because everything needs to be calculated every time – which is probably a bigger problem than the reactivity system
  • If the data tree is much larger than needed (for the current presentation). Lets assume we have a game with a few AI players. Each AI player is just presented as name, score and a few more fields. But the AI implementation may be several megabytes of complex data in memory. There is NO reason to make that data reactive out of laziness.

My basic design in AngularJS was:

  STATE ==> Presentation Data ==> Apply Filters ==> Apply Paging ==> Give to Angular

This means that the Presentation data is already structured quite much as the UI and its components, and it does not contain more data than necessary (or it does not have to, if it gives problems).

Disclaimer

I will be doing work based on this “reasearch” in the near future. I will update this post if I discover something relevant.

Leave a Comment


NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Time limit is exhausted. Please reload CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.