0.0.0 • Published 6 years ago

safer-vuex v0.0.0

Weekly downloads
3
License
MIT
Repository
github
Last release
6 years ago

safer-vuex

This library is a simple wrapper for vuex API. Its purpose is to provide better type safety when using vuex with typescript.

Installation

npm install --save safer-vuex

Example

Let's say we want to create a vuex store which will manage the state of a single numeric variable counter. We start by providing an interface and implementation for its state:

./root-state.ts

export interface IRootState {
    counter: number
}

export const rootState: IRootState = {
    counter: 0
}

Next we define accessors for its getters, mutations and actions in a separate file.

./root.ts

import { createRootOptions, AccessorFactory } from "safer-vuex"
import { rootState, IRootState } from "./root-state"

// first we instantiate a factory instance which we will use to create accessors.
const f = AccessorFactory.root<IRootState>()

// now we create an object with accessors
export const root = {
    getters: {
        counter: f.getter<number>(state => state.counter)
    },
    mutations: {
        setCounter: f.mutationWPayload<number>((state, payload) => (state.counter = payload)),

        incrementCounter: f.mutation(state => state.counter++),

        decrementCounter: f.mutation(state => state.counter--)
    },
    actions: {
        countdown: f.action<number>(context => {
            return new Promise<number>(resolve => {
                const interval = setInterval(() => {
                    // here we are alraeady accessing getter through our accessor with type safety
                    // `true` flag will be explained later
                    const currentValue = counter.getters.counter.get(context.getters, true)
                    log.debug(currentValue)

                    if (currentValue === 0) {
                        clearInterval(interval)
                        resolve(currentValue)
                    } else {
                        counter.mutations.decrementCounter.commit(context)
                    }
                }, 1000)
            })
        })
    }
}

// Now we can use our `root` object with accessors to generate the actual StoreOptions<IRootState> object for
// vuex API. `createRootOptions` function expects an object which conforms to the
// IAccessorsBundle<IRootStore, any> interface so type checking is performed here.
export const storeOptions = createRootOptions(rootState, root)

We exported two objects from root.ts: root and storeOptions. storeOptions is only required for store initialization, but root is later used for access. storeOptions now looks like this.

{
    getters: {
        counter: state => state.counter
    },
    mutations: {
        setCounter: (state, payload) => state.counter = payload,
        incrementCounter: state => state.counter++,
        decrementCounter: state => state.counter--
    },
    actions: {
        countdown: context => {
            return new Promise<number>(resolve => {
                const interval = setInterval(() => {
                    const currentValue = root.getters.counter.get(context.getters, true)
                    log.debug(currentValue)

                    if (currentValue === 0) {
                        clearInterval(interval)
                        resolve(currentValue)
                    } else {
                        counter.mutations.decrementCounter.commit(context)
                    }
                }, 1000)
            })
        }
    }
}

This is the StoreOptions<IRootState> object which can now be used to initialize vuex store.
./store.ts

import Vue from "vue"
import Vuex, { Store } from "vuex"
import { IRootState } from "./root-state"
import { storeOptions } from "./root"

Vue.use(Vuex)

export const store: Store<IRootState> = new Vuex.Store<IRootState>(storeOptions)

Store can be safely accessed with accessors we defined in root object.

import { root } from "... wherever root.ts is located relatively to this file ..."

// ... somewhere inside vue component
const store = this.$store

let currentCounterValue = root.getters.counter.get(store.getters)
log.debug(currentCounterValue) // 0

root.mutations.setCounter.commit(store, 2)

currentCounterValue = root.getters.counter.get(store.getters)
log.debug(currentCounterValue) // 2

root.mutations.incrementCounter.commit(store)

currentCounterValue = root.getters.counter.get(store.getters)
log.debug(currentCounterValue) // 3

root.actions.countdown.dispatch(store).then(value => {
    log.debug(value) // 0
})

As you can see, getters, mutations, and actions are used by calling get, commit and dispatch methods respectively on their accessor objects. commit and dispatch must be provided with context or store object. They will internally call vuex's commit or dispatch function with full namespace and mutation/action name. get on the other hand receives getters object and an optional second parameter local. local is false by default. If you set it to true, getter will be called only by name, not the full path (namespace + "/" + name). You can set this flag when you call getter of a namespaced module inside its own context (see action accessor in the example above).

Modules

Modules are created in a similar fashion. There are two differences:

  1. AccessorFactory is instantiated by calling module(namespace?: string) static method and optionally providing namespace if module should be namespaced.
  2. Instead of createRootOptions, createModule is called to generate Module object for registration in vuex store.

API

Here is the summary of all factory methods available for the creation of accessors.

/**
 * Create getter accessor.
 */
getter<ReturnT>(
    func: (state: StateT, getters: any, rootState?: RootStateT, rootGetters?: any) => ReturnT,
    name?: string
)

/**
 * Create mutation accessor.
 */
mutation(
    func: (state: StateT) => void,
    name?: string
)

/**
 * Create mutation accessor with payload.
 */
mutationWPayload<PayloadT>(
    func: (state: StateT, payload: PayloadT) => void,
    name?: string
)

/**
 * Create action accessor.
 */
action<ReturnT>(
    func: (context: ActionContext<StateT, RootStateT>) => Promise<ReturnT>,
    name?: string
)

/**
 * Create action accessor with payload.
 */
actionWPayload<PayloadT, ReturnT>(
    func: (context: ActionContext<StateT, RootStateT>, payload: PayloadT) => Promise<ReturnT>,
    name?: string
)

Each factory method receives optional second parameter name. We didn't use this parameter in our example. If provided, name will be the actual key used in the generated StoreOptions or Module object. If you don't provide it, accessor's key will be used implicitly. For example, if we had defined our counter getter like this:

// ...
getters: {
    counter: f.getter<number>(state => state.counter), "getCounter"
}
// ...

the actual StoreOptions object would then look like this:

// ...
getters: {
    getCounter: state => state.counter
}
// ...