A Vue Retrospective

This is the English version of the article.

この記事の日本語版はこちら

Two years ago I wrote an article bashing Vue 2 and the recommended tooling for it in several ways. Since then, I have moved on to Vue 3 and employed different tools to resolve all those issues. There are no outstanding architectural problems directly related to Vue at this point.

Wow, never thought I would actually say that.

This is how I resolved the problems from the previous article.

Template Type Safety

Two years ago, Vue 2 offered no type safety for TypeScript code inside templates. This caused dozens of small, meaningless bugs that were undiscoverable without test code, Storybook code, or a manual test on the deployed website.

At first we only found a partial fix. We were using VSCode as the primary IDE, and the only production-ready extension was Vetur. Since Vue itself doesn’t provide a “tool” for type-checking, it was up to Vetur to provide that functionality. Vetur released some experimental parameters for evaluating types in the template area of each component, but the support didn’t function for a lot of edge cases, and fell flat completely for types defined in other Vue files.

A few months ago, I discovered a different extension altogether: Volar. This extension supports complete type safety for libraries, as well as types declared in different files in general. As a result, we have attained complete type safety for the entire Vue component file.

Global Variables

In order to use global variables in the template portion of a Vue component, you must declare them on the component itself.

We didn’t solve this problem because after adopting the composition API, it became non-issue.

The main reason why you might have a lot of variables or functions re-declared on the component is if you’re performing computations in the template. We moved most computed variables to be represented by hooks, easily reusable in Vue 3’s composition API. This conveniently eliminates the need for components, much less component templates, to know about the various data sources and helper functions required.

Vuex Is Too Complex

We stopped using Vuex altogether. That was the easy part.

Replacing Vuex took some time. The current implementation is a dynamic store-like abstraction that works using inject and provide. For example, let’s say you’re storing the user as a piece of shared state. There are hooks to create the context in which the user can be accessed or modified, and then subsequently access that context.

Realistically, this is what the code looks like:

import { inject, InjectionKey, provide, readonly, Ref, ref } from 'vue-demi';

const USER_INJECTION_KEY: InjectionKey<UserContext> = Symbol('user');

export function useUser(user: Ref<User | null>) {
  const context = {
    user: readonly(user),
  };
  
  provide(USER_INJECTION_KEY, context);
  return context;
}

export function useCurrentUser() {
  return inject(USER_INJECTION_KEY, {
    user: readonly(ref(null)),
  });
}

export type UserContext = ReturnType<typeof useUser>;

(The type definition appears circular but compiles and typechecks correctly.)

Other data is shared in the same way using an InjectionKey. This pattern is particularly interesting for several reasons:

  1. It’s totally type-safe. You know exactly what’s going to come out and what operations are available on the data, unlike the old Vuex stores that had to get functionality re-declared.
  2. It’s quite friendly to Storybook stories. Components that depend on the current user can be trivially mocked out by called useUser with the desired mock user in each story. More common combinations can be further hook-ified, like useMockUser or useMockAdminUser, leading to Lego-like construction of complex component requirements.
  3. Some previously intractible features are instantly trivialized. You can imagine a case where an admin wants to see a screen from the perspective of a different user. With the code above, you simply call useUser, and then everything below that in the component tree uses the new user. Changing the context in which a component exists is simple and transparent.

I don’t foresee ever going back to Vuex when we can write well-typed “stores” in basic TypeScript.

Conclusions

Vue has certainly grown over the last two years. I do own a number of legacy, Vue 2 projects at this point, and it’s unclear how or when they will get ported to Vue 3. But new projects have quite a solid base to start from.

The latest Vue is the best it has ever been. If your organization is leaning towards it, now is a good time to give it a try.