Remember the days when we could leave our houses and go to events? Feels like so long ago, but in reality, VueConf US 2020 was just over a month ago. With more than 900 attendees, VueConf was two action-packed days of great talks with topics ranging from design systems to Vue 3’s hot new Composition API. Being in a room with everyone gave me an important reminder—the Vue community is full of amazing, friendly people who are all eager to keep learning and improving. If that were the only thing, Vue would be a fantastic framework to use—but it’s also just a spectacular framework in general.
I also had the opportunity to present my talk, Unconventional Vue—Vue as a Backend Framework at VueConf. When I first took a hard look at Vue 3 and saw the addition of the new Observability API, I knew there was certainly some tomfoolishery that could be done if I thought outside of the frontend box. This new API exposes Vue’s (previously) internal observability model. It’s the thing that causes your view to update if the data in your Vue component changes. I figured that I could use it to power some real-time data animations, make a chat client, or even better—trigger AWS Lambda functions as app data changes. Thus, my talk was born.
With a title like Vue as a Backend Framework, I knew that many who listened would be fairly skeptical about my talk, figuring that I was playing around with server-side rendering or something like that, or another story we’ve been told before. But it seemed to be largely well-received! As long as someone was able to learn at least one thing from my talk, I’m happy.
I thought it might be helpful to share the gist of my presentation in written form. My presentation focused on observability and reactivity in Vue. Vue 3 opens up a whole new realm for exploration. Let’s dive in.
Observability in Vue 2.x
new Vue({
data() {
return {
foo: 'Vue'
}
}
})
When we create a new Vue component and we write our data function, we don’t think much about it. To us, it’s just some data used by our component. Under the hood, though, a little bit of magic happens. Vue iterates over all of the properties of our data object, and sets up some observers. These observers watch our data, and as it changes, it alerts the view that our component needs to re-render. Vue calls the mechanism that tells components to re-render the “Watcher.” Internally, it’s implemented roughly like this:
// internally, something like this happens
for (let key in Object.keys(data)) {
Object.defineProperty(this, key, {
get() {
addWatcherDependency(key)
return data[key]
},
set(value) {
markDirty(key)
data[key] = value
}
})
}
Object.defineProperty is used to set up getters and setters for every property in our object, with the getters setting up dependencies that need to be tracked, and setters alerting that a property has been changed. If a subtree of our component depends on a property in our data object and we change the value, Vue will re-render that subtree.
While this approach has worked fairly well for some time, it does have limitations. The main one that people care about is that all of the top-level properties that our component will access must be defined when we create the component. Vue.set will let us add new properties to a nested object in a reactive way, but not at the top level. While this limitation isn’t huge, it would be nice if we could dynamically add properties and have our component reference them.
Vue 2.6’s Vue.observable
Vue 2.6 exposed Vue’s internal observability module in the form of Vue.observable. Essentially, this is the function that gets called with the result of your component’s initial data function. Since we now have direct access to this, we can use it to do things like write simple cross-component stores. Additionally, if we were to write the render functions of some Vue components by hand, we could use Vue.observable to make them reactive. In this following example, try setting the colors to purple or green.
See the Pen Vue.observable by Oscar Spencer (@ospencer) on CodePen.
These two components are able to share the same data store and update each other’s colors.
Reactivity in Vue 3
When Vue 3 launches, it will ship with a completely standalone reactivity module that can be used anywhere. It’s completely framework-agnostic, so if you wanted to write your own frontend framework that had observables, you could use the one from Vue without having to write it yourself. Wonderfully, it even removes all of the limitations of the old observability module. That’s possible because it’s written with the Proxy object from ES6. The implementation looks a little something like this:
// internally, something like this happens
new Proxy(data, {
get(obj, prop) {
createWatcherDependency(prop)
return obj[prop]
},
set(obj, prop, value) {
markDirty(prop)
obj[prop] = value
}
})
With this, Vue can now detect any change to an object, including the addition of new properties. Here’s an example of that standalone module in action:
import { reactive, effect } from '@vue/reactivity'
const counter = reactive({ num: 0 })
let currentNumber
effect(() => {
currentNumber = counter.num
})
console.log(currentNumber) // 0
counter.num++
console.log(currentNumber) // 1
The two key bits here are reactive and effect. You can pass a plain object to reactive, and it will be all set to trigger effects as the object changes. When we call effect, Vue registers that the function we gave it depends on the num property of counter. On line 13 when we mutate num, the effect is triggered again and the currentNumber variable gets updated. What’s maybe even more fun is that it’s smart enough to know when we use everyday methods on our reactive objects, and can understand more complex changes:
import { reactive, effect } from '@vue/reactivity'
const myMap = reactive({ foo: 1 })
let keys
effect(() => {
keys = Object.keys(myMap)
})
console.log(keys) // [ 'foo' ]
myMap.bar = 2
console.log(keys) // [ 'foo', 'bar' ]
In this example, we use Object.keys to get the keys of the object. The reactivity module is smart enough to know that because we used Object.keys, our effect needs to be triggered any time a new key is added or removed.
Purely the existence of this module begs the question: what new things could we do with this? Surely we could make some interactive animations that have effects trigger based on real-time data. We could build a notification system that alerted users when certain events occurred. But could we build a full backend? That’s exactly what I did.
The Vue Backend
I set up a simple Express.js server for a user management app, with the usual suspects for methods:
POST /users
PATCH /users/:name
DELETE /users/:name
POST /superusers
DELETE /purge
The POST /users route looks like this:
app.post('/users', (req, res) => {
database.push(req.body)
res.sendStatus(201)
})
And my amazing in-memory database looks like this:
const { reactive } = require('@vue/reactivity')
module.exports = reactive([])
It’s just a plain JavaScript array that’s been made reactive.
In its current state, it’s a little boring. What makes it exciting is that we can register effects that will get triggered whenever certain data in our database changes. For my presentation, I wrote a pretty fun one, called tweet:
tweet() {
const record = database[database.length - 1]
if (!record) return
if (!record.silent) request.post(LAMBDA_URL, {
json: {
op: 'tweet',
status: `${record.name} has joined the community!`
}
}, (error, response, body) => {
record.tweetId = body.id_str
console.log('Tweet sent with id', body.id_str)
})
},
This effect looks for the latest entry in the database, and then sends a request to an AWS Lambda function to tweet that a new user has joined our community. Since deleting tweets is all the rage, we also have a yeet effect that does just that when a user is deleted from our database:
yeet() {
for (let record of database) {
if (record && record.yeet && !record.yeeted) {
request.post(LAMBDA_URL, {
json: {
op: 'yeet',
tweetId: record.tweetId
}
}, (error, response, body) => {
if (!error) {
record.yeeted = true
console.log(record.name, 'yeeted successfully.')
}
})
}
}
},
Registering these effects is as simple as
effect(tweet)
effect(yeet)
And we’re off to the races! This is pretty neat (to me, anyway). We’ve now got Vue triggering an AWS Lambda function every time we add or remove records from our database.
There’s one more effect that I think is worth showing. Check out this route for POST /superusers:
app.post('/superusers', (req, res) => {
const record = reactive(req.body)
effect(initCreateSpreadsheet(record))
database.push(record)
res.sendStatus(201)
})
For this route, I’m imagining that once we have a superuser, they’ll want a fancy spreadsheet made for them whenever they log in. In this case, this is an effect that’s registered for a specific record. You can take a look at that effect here:
initCreateSpreadsheet(user) {
return () => {
if (user.online) {
console.log('Generating spreadsheet...')
setTimeout(() => console.log('Done.'), 4000)
}
}
},
While we’re not actually generating anything here, we certainly could, and it would run whenever the user’s online flag became true. For my demo app, I included two more effects, and one of them gets chained off of the first effect. If you would like to see that, you can check out the full source for this example.
So that’s pretty much the gist of what I covered in the talk. Vue might typically be used as a frontend framework, but Vue 3 opens a whole world of possibilities for the backend and more. What I’ve shared here are just a few examples. If you are interested in playing around with this yourself, you can find the source code for the demo app here.