Type-safe Vuex State Usage in Components (Without [as many] Decorators)
Tim Bendt
Posted on January 17, 2019
If you've started a Vue project and used Typescript recently you are probably familiar with the vue-class-component or maybe the more extensive alternative, vue-property-decorator. These libraries provide decorators, which are currently supported in Typescript, to allow typescript developers to write vue components using a ES6 class syntax and a declarative "Decorator" syntax which picks up a property or method and wraps it in specific functionality, like @Prop
before a plain class member makes the decorated property a member of the final component props object during runtime.
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class PhotoCard extends Vue {
@Prop()
public url!: string;
public imageSrc: string = this.url || "";
public isEditing: boolean = false;
public editPhoto() {
this.isEditing = true;
}
public cancel() {
this.isEditing = false;
}
}
I can't say for sure that the decorator way of writing Vue components is completely superior to the classic way of defining components using Vue.component()
, but it does make the syntax a little more flexible and more type-safe when you are referencing other parts of the component from inside it's members.
Now, once you've decided to dive into the "typescript way" of writing Vue apps you may find yourself wondering how to make everything typesafe. This comes up quickly when you are dealing with Vuex. Because, Vuex is a fairly "indirect" programming paradigm. You have to infer the presence of state values by looking at the source of the Vuex store itself. And you have to map to mutations and actions using "magic strings" inside of your component code. 🤮
You may have seen the project vuex-class which makes it possible to map members of your component to your store using decorators, but this doesn't really offer you a truly type-safe coupling between the store and it's usage in the components. It's still "indirect". You have to define the parameter types and return types yourself inside the component by looking at the source code of the vuex store. Eg:
const authModule = namespace("auth");
// ...
@authModule.State
public authenticated: boolean;
@authModule.State
public user: IUser;
@authModule.Action
public handleReset: (email: string) => Promise<any>;
public handleResetPassword() {
this.handleReset(this.user.email).then(() => {
this.showResetInstruction = true;
});
}
I discovered a better library for solving this problem recently and refactored a project to use it, so, I will share with you how to use the vuex-module-decorators instead.
This approach works best if you are writing modules to break up your vuex store into multiple code files.
When you write and define your store modules you will use a class decorator just like when you write a vue class component.
import { Module, VuexModule, Mutation, Action } from "vuex-module-decorators";
import Store from "../index";
@Module({
dynamic: true,
store: Store,
name: "userAdmin",
namespaced: true,
})
export default class UserAdminModule extends VuexModule {
public userResults: GetUserDto[] = [];
public rawResultSet: GetUserDto[] = [];
// ...
public get historyForUser(): GetAuditDto[] {
// ...
}
@Mutation
public loadResults({ data }: AxiosResponse<GetUserDto[]>) {
if (data && data.length) {
this.rawResultSet = data;
}
}
@Action({ commit: "loadResults"})
public async searchUsers(organization: string) {
return await ApiClient.getUsers({ organization: organization || "1", sort: "name"});
}
}
This module will be inserted into the parent store dynamically at run-time. Any "getter" in the class will be turned into a member of the "getters" on the vuex module. The @Mutation
decorator does what it says on the tin. Inside a mutation function you modify properties of the store by using the friendly and strongly typed this.
syntax. And the @Action
decorator will automatically call the specified mutation after the promise resolves from the async function it is decorating, cutting down on the boilerplate code of writing actions that call mutations when they are done.
So... how do you access this module in a typesafe way on your components?
Like this. You use the getModule()
function in your component to get a typed instance of the module.
import { getModule } from "vuex-module-decorators";
import UserAdmin from "@/store/modules/UserAdmin";
const userAdminState = getModule(UserAdmin);
@Component({
components: {
// ...
},
})
export default class AdminUsersSearch extends Vue {
public loading: boolean = false;
public get filteredUsers(): GetUserDto[] {
return userAdminState.filteredResults;
}
public mounted() {
this.loading = true;
userAdminState.searchUsers("1").then(x => this.loading = false);
}
}
Note: if you didn't create a "dynamic" module you will need to pass the store into the getModule function - see the docs.
In your typescript code, as soon as you type userAdminState.
you immediately get intellisense (in VS Code obv.) which shows you what the state, getters, mutations, and actions are available on the state module. If you need to change some property of the vuex module, you will get compiler errors everywhere that you use that property of the module in any component, making it harder to break your app when you refactor your state code.
Posted on January 17, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.