Creating a preview mode in a Vuex app with dependency injection

How I implemented a preview mode in a VueJS application

In this article, I will talk about how to use dependency injection (DI) in a Typescript Vue app with Awilix.

Before I go ahead and just talk about how to implement it, I will explain why I use DI in the project and what I want to achieve.

The problem

Some months ago, I got a ticket to implement a preview mode in a widget repository. This widget is a checkout widget where the user will fill out some forms to pay for the product.

This widget is made with VueJS and is using Vuex to create some modules. Almost all modules have one action that will trigger one request, set the loading state while waiting for the response, then it will get its data and save it in the module together with a success state. If the request fails, it will set the status as failed in the module.

So, to implement the preview mode I have to replace all these requests with mock data. For example, instead of fetching the API to validate the payment to get the response, it will return an object with a fake success response to prevent this request.

This way, the user can navigate through the widget flow without any request being made, removing the risk of changing something wrongly.

How to achieve the preview mode

First, let’s check how to do this preview mode without DI.

Not using Dependency Injection

As I mentioned, the preview mode consists of showing the app workflow while it is being used, but instead of displaying real data, it will display fake data.

To do that, we must only return fake data instead of hitting the real server. Doing that is very simple, you just need to add a verification on each server request to return fake data when the preview mode is enabled.

Before
const data = await requestDataFromServer()
After
const data = isPreviewMode ? mockedData : await requestDataFromServer()

This would definitely solve the problem but I don’t think is the best thing to do because we will need to add this verification on all requests.

Also, the preview mode logic will be spread in a bunch of files and we would add more complexity to each file. Because now, we must add a new responsibility: check if the preview mode is enabled.

Thinking in a SOLID way, it’s always good to avoid this situation where a file has multiple responsibilities because that leads to a complex code with a bunch of logic, huge tests, worse readability, and hard to add a new feature or even fix a bug.

The last thing is if someone adds a new request in another file without knowing about the preview mode, it may break the preview mode. Because now, in one place of the app it will make a request instead of using a mock data.

Using Dependecy Injection

I’m not gonna lie, I love the concept of DI and I always want to use it. Therefore, in this task DI was the first thing that came to my mind. But before that, I analyzed if it was worth doing, since by using it I would’ve had to refactor a bunch of files and if they had no tests, I would’ve had to create them to make sure everything would work after this big refactor.

But let’s talk about DI, with it we can create a container where you can register some class or function with the implementation we need.

Let’s go to an example:

Imagine we have an app that will validate your credit card. In this case, we can create an interface to specify how this service will work.

// src/services/CreditCardService.d.ts

export interface CreditCard { 
    number: number;
    holderName: string;
    expiration: {
        month: number;
        year: number;
    }
    cvv: number;
}

export interface CreditCardService {
    valid(creditCard: CreditCardData): Promise<boolean>;
}

Here I created two interfaces, one with the credit card information (CreditCard) and another one with the service contract (CreditCardService). So, the service will have one method called valid that will receive the credit card data and returns a boolean from a Promise saying if the credit card is valid or not.

I’m assuming this will return a Promise because you may use an API to validate the credit card.

Now, let’s implement it! The first thing is to create the class that will implement the CreditCardService interface. To do that you only need to add implements CreditCardService after the class name. It will show an error saying that you must implement the CreditCardService interface.

To implement it you must add a valid method with the same interface signature.

import { CreditCardService } from '@/services/CreditCardService'

class CreditCardApiService implements CreditCardService {
    valid(creditCard: CreditCard): Promise<boolean> {
        return fetch('https://my-credit-card-api/v1/validate', {
            method: 'POST',
            body: JSON.stringify(creditCard)
        })
            .then(response => response.json())
            .then(data => {
                return data.isValid
            })
    }
}

export default CreditCardApiService

Now, to use it let’s create a container that will hold this credit card service.
Here I will create an interface for the container called Container that will have all the service signatures. Then, I will create our container using this interface:

import CreditCardApiService from '@/services/CreditCardApiService'
import { CreditCardService } from '@/services/CreditCardService'

interface Container {
    creditCardService: CreditCardService;
}

const container: Container = {
    creditCardService: new CreditCardApiService(),
}

export default container

To use the credit card service we must use the container:

import container from '@/container'

const { creditCardService } = container;

await creditCardService.valid(myCrediCard) // true or false

In order to add this preview mode, we must:
1 – Create a CreditCardMockedService class that will implements CreditCardService interface
2 – Add a verification in the container creating to user CreditCardApiService or CreditCardMockedService

1)

import { CreditCardService } from '@/services/CreditCardService'

class CreditCardMockedService implements CreditCardService {
    valid(creditCard: CreditCard): Promise<boolean> {
        return Promise.resolve(true)
    }
}

export default CreditCardMockedService

2)

import CreditCardApiService from '@/services/CreditCardApiService'
import CreditCardMockedService from '@/services/CreditCardMockedService'
import { CreditCardService } from '@/services/CreditCardService'
// image we have a config file that will hold this info
import { isPreviewMode } from '@/configs'

interface Container {
    creditCardService: CreditCardService;
}

const container: Container = {
    creditCardService: isPreviewMode ? new CreditCardMockedService() : new CreditCardApiService(),
}

export default container

This way, the one who is using the container doesn’t know what is the real creditCardService implementation. It doesn’t even know the CreditCardMockedService and CreditCardApiService exist. It only knows that creditCardService.valid will validate the credit card.

With this strategy we can put the preview mode logic in the container creation, we just need to create a class (or function) with the real implementation and another with mock data. One good thing is that we will have two files, each file with your logic and context, with fewer responsibilities and easier to test.

Let’s check each file’s responsibility:

  • CreditCardApiService -> it will use some API to check if the credit card is valid or not.
  • CreditCardMockedService -> it will always say that the credit card is valid.
  • container -> it will use the real or mocked implementation according to isPreviewMode variable.
  • Who is using the container -> it will use creditCardService to know if a credit card is valid or not.

A quick glance at SOLID

As you can see, now we will have smaller pieces of code, each one with it’s own responsibility, which makes your code more SOLID.

By the way, if you don’t know what SOLID is I advise you to search for materials about it. This will make your code clearer, and easier to test and understand. Here is a Sandi Metz video that I always recommend for developers that don’t know about SOLID: GORUCO 2009 – SOLID Object-Oriented Design by Sandi Metz.

Now, going back to the main subject.

The Vue app

Before diving into the DI implementation, let’s talk about the widget/project specs. In this project we are using:

As I mentioned before, the app’s logic was inside the vuex modules, and they were structured like this:

- src
    - store
        - modules
            - config
            - reservation
            - confirmation
            ...

The config contains all widgets configs that will be different according to each client or scenario. All the other modules contain app logic like:

  1. Some user or event will trigger a module’s action, setting the loading state
  2. This action will make a request that may use another module state
  3. The result of this action will update the module state, either for success or error state

Another detail, the whole store was not using typescript ๐Ÿ˜•. So, I first have to add types to all of them, and to do that I use vuex-module-decorators (v2.0.0) because its works like vue-property-decorator that we are using to create the components.

After refactoring all vuex modules with Typescript I started the DI implementation. The first thing to do was install awilix package, I choose this library because it’s small and simple to use and set up it.

Sample app

To show you guys the code and a real example, I created a repository on Github with the same widget pattern.

In this demo, you can search and list all users from Github and their repositories. You can change to preview mode to return mocked data.

So, when searching for a user name, it will request Github’s API, which will return a list of users. When you select a user, the app will do another request to Github’s API to get all the repositories to list in the app.

In this sample app, I created a container that will have a sourceCodeService property, which will return a service that contains methods to get the users (searchUser) and repositories (searchUser), and it will have a property name that contains the service name (Github).

I called it sourceCodeService instead of githubService because this way I can create an implementation for any service like Github. For example, if we want to add support to GitLab, we just need to create a GitLabApiService and set it in the sourceCodeService property from the container.

Implementing DI in the Vue app

Going back to the widget, one important thing to mention is that the preview property is on the config vuex module. When the widget is mounted it will trigger a request to get the configs from the server and set it in the module. The only way to know if the widget is with the preview mode enabled or not is by accessing the config vuex module.

The next step is to decide where to put my container creation. Initially, I was thinking to add this file at src/container.ts but since I must get the config vuex module to know if the preview mode is enabled or disabled, I decided to create a new vuex module that will hold the container. This way, I can get the preview mode value and create a container with this information.

Creating the container
import { Action, Module, Mutation, VuexModule } from '@/store/modules/vuex-module-decorators'
import createContainer, { ContainerType } from './createContainer'

@Module({
  namespaced: true
})
class Container extends VuexModule {
  current!: ContainerType['cradle']

  @Mutation
  setContainer(newContainerCradle: ContainerType['cradle']): void {
    this.current = newContainerCradle
  }

  @Action
  updateContainer(): void {
    const { commit, rootState } = this.context
    const { isPreviewMode } = rootState.configs
    const newContainer = createContainer({ isPreviewMode })

    commit('setContainer', { ...newContainer.cradle })
  }
}

export default Container

Source: src/store/modules/container/index.ts

This module will hold the container’s cradle, which is an object that will contain the implementations. Something like:

{
    myService: myServiceImplementation,
    ...
}

This way, I can access the current state from the container vuex module in other modules.
Here is the createContainer implementation that this module is using:

import { AwilixContainer, asClass, createContainer } from 'awilix'

import GithubApiService from '@/services/sourceCode/github/GithubApiService'
import SourceCodeMockService from '@/services/sourceCode/SourceCodeMockService'
import { SourceCodeService } from '@/services/sourceCode/SourceCodeService'

type ContainerDependencies = {
  sourceCodeService: SourceCodeService
}
type ContainerType = AwilixContainer<ContainerDependencies>
type Props = {
  isPreviewMode: boolean
}

const createDIContainer = (props: Props): ContainerType => {
  const { isPreviewMode } = props
  const container = createContainer<ContainerDependencies>()

  container.register({
    sourceCodeService: asClass(isPreviewMode ? SourceCodeMockService : GithubApiService)
  })

  return container
}

export { ContainerType }
export default createDIContainer

Source: src/store/modules/container/createContainer.ts

The createDIContainer is the function that the module is calling and it will receive an isPreviewMode property to know which implementation will be injected: SourceCodeMockService or GithubApiService. Here, I’m using awailix to create the DI container.

The last piece of the container creation is to trigger the updateContainer action from the Container vuex module. Otherwise, the current state will always be undefined. To achieve this I created a vuex plugin:

import { StoreType } from '@/store/Store'

const containerPlugin = (store: StoreType): void => {
  const updateContainer = (): void => {
    store.dispatch('container/updateContainer')
  }

  // Initial setup
  updateContainer()

  store.subscribe((mutation): void => {
    if (mutation.type === 'configs/setConfigs') {
      updateContainer()
    }
  })
}

export default containerPlugin

Source: src/store/plugins/container/index.ts

This plugin will trigger the updateContainer action through calling store.dispatch('container/updateContainer'). As you can see, there are two moments when I update my container:

  1. When the store is mounted
  2. When the configs change

To know when the configs were being changed, I added a subscription to the store. That will be triggered on every mutation, then I just check if the mutation is the one that will change my configs.

Now my container is working and synced with my configs. The next step is to consume this container in another module.

Consuming the container

As I mentioned, in this repo the user will select a Github user by typing a name, then a request will be made to the Github API and I will list the users that will come from this response. So, here I create a vuex module to hold this logic:

import { Action, Module, Mutation, VuexModule } from '@/store/modules/vuex-module-decorators'

import { SourceCodeServiceUser } from '@/services/sourceCode/SourceCodeService'
import { getErrorMessage } from '@/selectors/error'

type Status = 'idle' | 'loading' | 'success' | 'error'

@Module({
  namespaced: true
})
class SourceCodeUsers extends VuexModule {
  users: SourceCodeServiceUser[] = []
  status: Status = 'idle'
  errorMessage: string | null = null

  get hasError(): boolean {
    return Boolean(this.errorMessage)
  }

  get isLoading(): boolean {
    return this.status === 'loading'
  }

  @Mutation
  setPending(): void {
    this.users = []
    this.errorMessage = null
    this.status = 'loading'
  }

  @Mutation
  setFulfilled(newUsers: SourceCodeServiceUser[]): void {
    this.users = newUsers
    this.status = 'success'
  }

  @Mutation
  setError(errorMessage: string): void {
    this.errorMessage = errorMessage
    this.status = 'error'
  }

  @Action
  async search(term: string): Promise<void> {
    const { commit, rootState } = this.context
    const { sourceCodeService } = rootState.container.current

    commit('setPending')

    try {
      const users = await sourceCodeService.searchUser(term)

      commit('setFulfilled', users)
    } catch (error: unknown) {
      const errorMessage = getErrorMessage(error)

      commit('setError', errorMessage)
    }
  }
}

export * from './composition'
export default SourceCodeUsers

Souce: src/store/modules/sourceCode/users/index.ts

The main thing here is the search action that will be triggered every time a user types a user name to search. To get the Container module I need to access this.context.rootState.container. Since this module has a current state that will hold the "real container", I must access it through this.context.rootState.container.current. The result of it will be the cradle that is an object with the services implementations.

Explaning with code:

@Action
async search(term: string): Promise<void> {
    const { rootState } = this.context
    const containerVuexModule = rootState.container // the vuex module
    const cradle = containerVuexModule.current // the container object
    const { sourceCodeService } = cradle // the container implementation for sourceCodeService

Now, we can use this sourceCodeService to search the github user:

const users = await sourceCodeService.searchUser(term)

Is important to mention that the SourceCodeUsers vuex module doesn’t know the real implementation.

Toggling preview mode

In this app, I created a component called AppConfigs that will render a switch to enable or disable the preview mode.

Every time it changes it will trigger the updateConfigs action from Configs vuex module:

import { Action, Module, Mutation, VuexModule } from '@/store/modules/vuex-module-decorators'

type AppConfigs = {
  isPreviewMode: boolean
}
@Module({
  namespaced: true
})
class Configs extends VuexModule {
  isPreviewMode = false

  @Mutation
  setConfigs(newConfigs: AppConfigs): void {
    this.isPreviewMode = newConfigs.isPreviewMode
  }

  @Action
  updateConfigs(newConfigs: AppConfigs): void {
    const { commit } = this.context

    commit('setConfigs', newConfigs)
  }
}

export * from './composition'
export default Configs

Source: src/store/modules/configs/index.ts

When this happens, it will also trigger my plugin that watches the mutations and consequently, it will re-generate the container.
Here is the flow when updateConfigs is triggered:
1 – It will update the Configs vuex module with new configs
2 – The containerPlugin will dispatch 'container/updateContainer'action
3 – The Container vuex module will re-create the container and set its cradle in current state.
4 – All places that are using the container will use the new implementation.

Conclusion

Using DI, we can avoid adding more complexity to each vuex module by extracting each logic in one class. I didn’t put any samples here but we could also have clearer tests (check here all the tests: link).

Another win is that now your vuex modules requests are using a service so they don’t know, for example, the fetch URL or anything about the request implementation, making it also easier to test.

I had a great time implementing this solution and I love the way it works!

But what are your thoughts about it? Do you like my implementation? Would you do something different? Don’t forget to comment here ๐Ÿ˜‰

Also, if you want to, you can fork my sample repo and try to add a GitLab or BitBucket integration ๐Ÿš€

Thanks for reading my article โค๏ธ

We want to work with you. Check out our "What We Do" section!