Test Driving the Vue Function API
DISCLAIMER: This article is not meant to be a guide on how to migrate your codebase to the new version of Vue. There are/will be plenty of those. It’s a collection of opinions, ideas, realizations, and emotions I had while trying the new Function API. If you’re unfamiliar with the proposal, check it out below.
API PROPOSAL:
https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md
Quick Take for People in a Hurry
- Prop type inference in fine for primitives, bad for arrays and objects
- I made a function that tricks TS into liking your props
- I prefer
state()
tovalue()
- Don’t use the spread operator (
...
) with reactive data unless know exactly what you’re doing
SSDB: Same Sh*t, Different Backpack
I recently bought a new backpack. My old one was dirty and the color was fading — nothing was particularly “broken” and I could probably get a few more years out of it if I wanted.
I wanted a replacement that had a similar capacity, familiar features and some improvements like waterproof seams and zippers. I also wanted something that looked cooler and more appropriate for a bike commuter in Boston. Let’s face it, we’re all a little bit vain 😎
I spent a couple weeks browsing Amazon and some nearby outfitters to find my new bag. I compared features and size and ultimately landed on something that fit my style, budget, and functionality requirements. I was excited about the idea of something new, but also conscious that I may not like it down the road.
My new backpack is taking some getting used to. I pack it differently, I don’t really know how to use every compartment yet, and I lose things in the bottom. I like it for the most part, but I’m still attached to my old pack that I now use on trips because I know its limits and where all the pockets are.
My feelings about the new Vue Function API are similar. It’s new, exciting, and looks easy enough to learn. However, when it comes to brass tacks, it just needs to work, and work as-good/better than the existing object-based API. Well, it does, but it’s taking some getting used to. The big win is better type safety (this is like waterproof seams in your code 😜), and the ability to organize and break out pieces of functionality.
My weekend was spent working on a medium-complexity side project. It includes vuex
, REST API, TypeScript and vue-router. I figured it was a good test-bed for the new Function API, so I tried it out on a couple small components.
Vue, TypeScript, and the Story So Far
I’ve been using Vue for 3 years at this point. I started with AngularJS and found Vue to be familiar, easier to use and more powerful, a rare combo! I have used Vue almost exclusively since then.
When you tie your entire codebase to a third-party abstraction, you diverge from common practice and open yourself up to maintainability issues down the road.
3 years ago was also when I started using TypeScript. It had a steeper learning curve and led to more than a few screen-punching episodes. Over time, I learned to love it! Mostly for the convenience of types. In many instances I’ve found an npm library’s index.d.ts to be more complete than their API Docs, they are very helpful for debugging.
Naturally, Vue+TypeScript was always the goal, but it’s been bumpy. I ended up using the vue-class-component style because, for a while, it was the ONLY way to combine both technologies. It was nice but it looked very different from the official object-based API, which makes it harder for people to switch and understand.
Then Vue 2.5 came out with a completely refactored type definition that made it possible to use the object-based API with relatively good typing. I tried that but ultimately reverted to the class-based API because it was what I’d been using for so long, it was proven, and the code had already been written. When you tie your entire codebase to a third-party abstraction, you diverge from common practice and open yourself up to maintainability issues down the road.
vue-function-api
package
Since the Function API is now only a proposal, and Vue 3 is sitting in the Soon™️ bin next to Half-Life and Portal 3, I had to use the plugin vue-function-api
. It’s an experimental port of the function API proposal for use in any Vue 2 app.
I really appreciated the ability to try a relatively new idea without anything being officially released, this is top-notch Developer Experience!
import { value, computed, onCreated, … } from ‘vue-function-api’
Installed like any other Vue plugin, this package exposes all of the hooks and wrappers mentioned in the proposal and promises that it behaves just as intended. Awesome!! The only expense is extra runtime computation, for now. The official API, combined with Vue 3 will be faster.
createComponent() type wrapper
If you are using TS, you will need to wrap your component definition in a createComponent()
call. This function effectively does nothing but return the component options you pass in, but it’s needed for TypeScript. It’s needed to infer types from props and set them correctly in your setup function. It adds friction, but hopefully it won’t always be needed.
Mad props
Defining props for both Vue and TypeScript was the first awkward point in my deep-dive…
Here we have a simple UserAlerts
component. Its job is to render the alerts that are on a users account. It accepts two props, a user
object and an array of alerts
.
As you can see, props
are defined like they always were. The value of each prop is the JS constructor of the “type” you expect. Vue will check the provided values against these at runtime. TypeScript also tries to use these options to type your props in setup
, but it’s sometimes unhelpful or wrong. To clarify, It’s perfectly fine for String, Booleans, and Numbers, but Arrays and Objects lead to issues inside the body of your component.
ArrayConstructor
is not a useful or accurate type for props.alerts
, I need to override it somehow while also keeping the runtime-compatible Array
type. I have to define the props myself, with the actual types. Cue interface
.
Now how do I tell TypeScript, separately from Vue, that these are my props?
I found 3 ways, none of them pretty:
Option 1: force-casting the props object
What is force-casting? It’s the TypeScript equivalent of a Jedi mind trick, but not as fun. Typically TS will infer a type through its initial value.
So with const foo = ‘some string’
, foo
is automatically a string
type. If, for some reason, the inferred type is wrong, you can sternly suggest a type using as
. This is called type-casting:
const anAlert = {} as Alert
TS is skeptical, but it trusts you. From this point on, it will treat anAlert
as an Alert
with all properties and methods. You and I know it isn’t, but sometimes this can be useful if you are building an object from scratch.
However, even this strategy has its limits. Consider the following:
const value = 42 as string[]
TS is going to call you out on your BS here. There is no way, in any universe, EVER, that the number 42
is an array of strings. But what if my code relies on that? What if I know that 42
will eventually be turned into an array of strings by some magical framework operating behind the scenes (I’m looking at you Vue). I can use a force-cast:
const value = 42 as any as string[]
I visualize the preceding line of code like a wizard waving their wand and turning a rock into a frog — it’s still a rock, but you see it as a frog.
This is how you outsmart TS; first you tell it that a value is of type any
and then you tell it what type you actually want. It’s ugly, but it’s necessary for what we are about to do:
So we’ve solved it! The only problem is that we had to define our props in two places. Once in the interface and once in the props definition. Keeping these in-sync will be difficult over time.
What if we could trick TypeScript in a different way.
Option 2: force-casting each prop
We will essentially do the same trick, but at the individual prop level.
While I like that I don’t have to write the name of a prop more than once, constantly writing as any as
can get tiresome, not to mention the code-smell. TypeScript developers pride themselves on how little they have to play the any
or as
cards to get things to work. I’ve managed to cram 6 violations in a third as many lines of code. GROSS!
A hack only becomes an anti-pattern if you do it more than once, so wrap the bad code in a good function, and you’re all set.
Option 3: ABSTRACTION!!!
Okay, taking stock, I like per-props casting because I only have to write my props once, but I hate writing as any as
.
Solution? Abstract away the ugly parts and keep the nice ones!
Can you guess what prop()
does? That’s right, (almost) Nothing! It returns the type you pass in but convinces TS that it is the type between the <>
, also known as a generic type. This is my implementation of prop()
:
There are lots of other ways to do this and I KNOW people are going to point them out, but this is my compromise, and I’m satisfied. A hack only becomes an anti-pattern if you do it more than once, so wrap the bad code in a good function, and you’re all set.
Supposedly, in Vue 3, you won’t even have to define props anymore, so you just need a plain old interface!
Is this a setup()!?
We’re here! This is where the magic happens! We get one function body to do EVERYTHING! No more methods, computed, data, watchers, or hooks. Also, no more this
! After all these years there is one common thread within Vue, everything will get bound to this
. Now they are getting rid of it. This is like when Angular killed $scope
. Crazy.
We no longer have a convenient (yet sometimes confusing) namespace to dump all of our crap, instead we get one function. What you do in this function is up to you, just remember that if you need to reference something in <template>
it needs to be returned at the bottom of the function. I forgot this many times.
Data, state, and value
This was my next stumbling point.
It took a while to figure out how to handle data in the setup function, and I’m still not sold on some things. Out of the gate, we have two strategies: state()
and value()
. The former wraps an object, and the latter wraps a single primitive value. I’m sure both are useful in different scenarios but I wanted to find the one that works for me.
Using value()
This is a quirk of the new system. A primitive value (string, number, etc.) cannot be made reactive on its own. In the following example there is no way to watch for changes to name
.
However, If you wrap name
and put its value inside an object to which you have a reference, then you can watch for changes to that object.
Lets see it in use in a component:
This part of the API always made me feel unwell. I don’t like the look of changing value
on every variable, I’d rather change a bunch of properties on one variable, like I used to do with this
. Thats why I like state()
Using state()
This feels more natural. I get to define my state under one namespace and have the stuff I want to change closest to the =
sign. I’m dealing less with an implementation detail and more like I’m just organizing the “sort” data under a single namespace.
State-spreading is like Man-spreading… Don’t
Now at some point you might be tempted to spread your state like this:
return { …sort, changeSort }
“Congrats! You’re an idiot! You can give up coding! You obviously don’t know how any of this works! And you’re not getting back the hour it took to figure out why this breaks stuff!”
— Me, to myself
This breaks reactivity flat-out. The sort
wrapper object has been sold for parts and is no longer useful, all primitive values are naked and afraid and getting shipped off to the template with no link back to where the values might be changed. This sucks, don’t do this.
So, this article is getting a little lengthy so I think I’ll leave it here for now and make a Part 2.
Conclusion
My overall feeling towards the Function API is trending positive, but my adoption is slower than I expected. I was able to work relatively efficiently with it after a while, but it was a bumpy ride and there were more than a few foot-guns. Part of this, though, is the fact that there is not a list of best-practices yet. The community is not teaching this and people are not sharing their work. This is the Wild West.
Here’s a picture of my backpack!