{"id":13683768,"url":"https://github.com/ktsn/vuex-smart-module","last_synced_at":"2025-04-12T19:46:18.554Z","repository":{"id":34154024,"uuid":"144475683","full_name":"ktsn/vuex-smart-module","owner":"ktsn","description":"Type safe Vuex module with powerful module features","archived":false,"fork":false,"pushed_at":"2023-01-21T13:01:50.000Z","size":2147,"stargazers_count":382,"open_issues_count":26,"forks_count":19,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-03T23:09:29.667Z","etag":null,"topics":["type-safety","typescript","vuejs","vuex"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ktsn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-08-12T14:54:22.000Z","updated_at":"2025-03-23T07:40:13.000Z","dependencies_parsed_at":"2023-02-12T09:50:19.887Z","dependency_job_id":null,"html_url":"https://github.com/ktsn/vuex-smart-module","commit_stats":null,"previous_names":[],"tags_count":26,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsn%2Fvuex-smart-module","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsn%2Fvuex-smart-module/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsn%2Fvuex-smart-module/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktsn%2Fvuex-smart-module/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ktsn","download_url":"https://codeload.github.com/ktsn/vuex-smart-module/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248625497,"owners_count":21135513,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["type-safety","typescript","vuejs","vuex"],"created_at":"2024-08-02T13:02:31.283Z","updated_at":"2025-04-12T19:46:18.532Z","avatar_url":"https://github.com/ktsn.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# vuex-smart-module\n\nType safe Vuex module with powerful module features. The basic API idea is brought from [Sinai](https://github.com/ktsn/sinai).\n\n## Features\n\n- Completely type safe when used with TypeScript without redundancy.\n- Provide a smart way to use modules.\n- Canonical Vuex-like API as possible.\n\n## Installation\n\n```bash\n$ npm install vuex-smart-module\n```\n\n## Usage\n\n**All examples are written in TypeScript**\n\nYou create a module with class syntax:\n\n```ts\n// store/modules/foo.ts\n\n// Import base classes\nimport { Getters, Mutations, Actions, Module } from 'vuex-smart-module'\n\n// State\nclass FooState {\n  count = 1\n}\n\n// Getters\n// Extend 'Getters' class with 'FooState' type\nclass FooGetters extends Getters\u003cFooState\u003e {\n  // You can declare both getter properties or methods\n  get double() {\n    // Getters instance has 'state' property\n    return this.state.count * 2\n  }\n\n  get triple() {\n    // When you want to use another getter, there is `getters` property\n    return this.getters.double + this.state.count\n  }\n}\n\n// Mutations\n// Extend 'Mutations' class with 'FooState' type\nclass FooMutations extends Mutations\u003cFooState\u003e {\n  increment(payload: number) {\n    // Mutations instance has 'state' property.\n    // You update 'this.state' by mutating it.\n    this.state.count += payload\n  }\n}\n\n// Actions\n// Extend 'Actions' class with other module asset types\n// Note that you need to specify self action type (FooActions) as a type parameter explicitly\nclass FooActions extends Actions\u003c\n  FooState,\n  FooGetters,\n  FooMutations,\n  FooActions\n\u003e {\n  incrementAsync(payload: { amount: number; interval: number }) {\n    // Actions instance has 'state', 'getters', 'commit' and 'dispatch' properties\n\n    return new Promise(resolve =\u003e {\n      setTimeout(() =\u003e {\n        this.commit('increment', payload.amount)\n        resolve()\n      }, payload.interval)\n    })\n  }\n}\n\n// Create a module with module asset classes\nexport const foo = new Module({\n  state: FooState,\n  getters: FooGetters,\n  mutations: FooMutations,\n  actions: FooActions\n})\n```\n\nThen, create Vuex store instance by using `createStore` function from `vuex-smart-module`:\n\n```ts\n// store/index.ts\n\nimport Vue from 'vue'\nimport * as Vuex from 'vuex'\nimport { createStore, Module } from 'vuex-smart-module'\nimport { foo } from './modules/foo'\n\nVue.use(Vuex)\n\n// The 1st argument is root module.\n// Vuex store options should be passed to the 2nd argument.\nexport const store = createStore(\n  // Root module\n  foo,\n\n  // Vuex store options\n  {\n    strict: process.env.NODE_ENV !== 'production'\n  }\n)\n```\n\nThe created store is a traditional instance of Vuex store - you can use it in the same manner.\n\n```ts\n// main.ts\n\nimport Vue from 'vue'\nimport { store } from './store'\nimport App from './App.vue'\n\nnew Vue({\n  el: '#app',\n  store,\n  render: h =\u003e h(App)\n})\n```\n\n### Nested Modules\n\nYou can create a nested module as same as Vuex by passing a module object to another module's `modules` option.\n\n```ts\nimport { Getters, Module, createStore } from 'vuex-smart-module'\n\nclass NestedState {\n  value = 'hello'\n}\n\nclass NestedGetters extends Getters\u003cNestedState\u003e {\n  greeting(name: string): string {\n    return this.state.value + ', ' + name\n  }\n}\n\nconst nested = new Module({\n  state: NestedState,\n  getters: NestedGetters\n})\n\nconst root = new Module({\n  modules: {\n    nested\n  }\n})\n\nconst store = createStore(root)\n\nconsole.log(store.state.nested.value) // -\u003e hello\nconsole.log(store.getters['nested/greeting']('John')) // -\u003e hello, John\n```\n\nNested modules will be [namespaced module](https://vuex.vuejs.org/guide/modules.html#namespacing) by default. If you do not want a module to be a namespaced, pass the `namespaced: false` option to the module's constructor options.\n\n```ts\nimport { Getters, Module, createStore } from 'vuex-smart-module'\n\nclass NestedState {\n  value = 'hello'\n}\n\nclass NestedGetters extends Getters\u003cNestedState\u003e {\n  greeting(name: string): string {\n    return this.state.value + ', ' + name\n  }\n}\n\nconst nested = new Module({\n  // nested module will not be namespaced\n  namespaced: false\n\n  state: NestedState,\n  getters: NestedGetters\n})\n\nconst root = new Module({\n  modules: {\n    nested\n  }\n})\n\nconst store = createStore(root)\n\nconsole.log(store.state.nested.value) // -\u003e hello\nconsole.log(store.getters.greeting('John')) // -\u003e hello, John\n```\n\n### Module Lifecycle and Dependencies\n\nGetters and actions class can have a special method `$init` which will be called after the module is initialized in a store. The `$init` hook receives the store instance as the 1st argument. You can pick some external dependencies from it. The following is an example for [Nuxt](https://nuxtjs.org/) + [Axios Module](https://axios.nuxtjs.org/).\n\n```ts\nimport { Store } from 'vuex'\nimport { Actions } from 'vuex-smart-module'\n\nclass FooActions extends Actions {\n  // Declare dependency type\n  store: Store\u003cany\u003e\n\n  // Called after the module is initialized\n  $init(store: Store\u003cany\u003e): void {\n    // Retain store instance for later\n    this.store = store\n  }\n\n  async fetch(): Promise\u003cvoid\u003e {\n    console.log(await this.store.$axios.$get('...'))\n  }\n}\n```\n\n\nThere are no `rootState`, `rootGetters` and `root` options on `dispatch`, `commit` because they are too difficult to type and the code has implicit dependencies to other modules. In case of you want to use another module in some module, you can create a module context.\n\n```ts\nimport { Store } from 'vuex'\nimport { Getters, Actions, Module, Context } from 'vuex-smart-module'\n\n// Foo module\nclass FooState {\n  value = 'hello'\n}\n\nconst foo = new Module({\n  state: FooState\n})\n\n// Bar module (using foo module in getters and actions)\nclass BarGetters extends Getters {\n  // Declare context type\n  foo: Context\u003ctypeof foo\u003e\n\n  // Called after the module is initialized\n  $init(store: Store\u003cany\u003e): void {\n    // Create and retain foo module context\n    this.foo = foo.context(store)\n  }\n\n  get excited(): string {\n    return this.foo.state.value + '!' // -\u003e hello!\n  }\n}\n\nclass BarActions extends Actions {\n  // Declare context type\n  foo: Context\u003ctypeof foo\u003e\n\n  // Called after the module is initialized\n  $init(store: Store\u003cany\u003e): void {\n    // Create and retain foo module context\n    this.foo = foo.context(store)\n  }\n\n  print(): void {\n    console.log(this.foo.state.value) // -\u003e hello\n  }\n}\n\nconst bar = new Module({\n  getters: BarGetters,\n  actions: BarActions\n})\n\n// Make sure to have all modules in the store\nconst root = new Module({\n  modules: {\n    foo,\n    bar\n  }\n})\n\nconst store = createStore(root)\n```\n\n### Nested Module Context\n\nWhen there are nested modules in your module, you can access them through a module context.\n\nLet's say you have three modules: counter, todo and root where the root module has former two modules as nested modules:\n\n```ts\nimport { Module, createStore } from 'vuex-smart-module'\n\n// Counter module\nconst counter = new Module({\n  // ...\n})\n\n// Todo module\nconst todo = new Module({\n  // ...\n})\n\n// Root module\nconst root = new Module({\n  modules: {\n    counter,\n    todo\n  }\n})\n\nexport const store = createStore(root)\n```\n\nYou can access counter and todo contexts through the root context by using `modules` property.\n\n```ts\nimport { root, store } from './store'\n\n// Get root context\nconst ctx = root.context(store)\n\n// You can access counter and todo context through `modules` as well\nconst counterCtx = ctx.modules.counter\nconst todoCtx = ctx.modules.todo\n\ncounterCtx.dispatch('increment')\ntodoCtx.dispatch('fetchTodos')\n```\n\n### Register Module Dynamically\n\nYou can use `registerModule` to register a module and `unregisterModule` to unregister it.\n\n```ts\nimport { registerModule, unregisterModule } from 'vuex-smart-module'\nimport { store } from './store'\nimport { foo } from './store/modules/foo'\n\n// register module\nregisterModule(\n  store, // store instance\n  ['foo'], // module path. can be string or array of string\n  'foo/', // namespace string which will be when put into the store\n  foo, // module instance\n\n  // module options as same as vuex registerModule\n  {\n    preserveState: true\n  }\n)\n\n// unregister module\nunregisterModule(\n  store, // store instance\n  foo // module instance which you want to unregister\n)\n```\n\nNote that the 3rd argument of `registerModule`, which is the namespace string, must match with the actual namespace that the store resolves. If you pass the wrong namespace to it, component mappers and context api would not work correctly.\n\n### Component Mapper\n\nYou can generate `mapXXX` helpers, which are the same interface as Vuex ones, for each associated module by using the `createMapper` function. The mapped computed properties and methods are strictly typed. So you will not have some typo or pass wrong payloads to them.\n\n```ts\n// @/store/modules/foo\nimport { Module, createMapper } from 'vuex-smart-module'\n\n// Create module\nexport const foo = new Module({\n  // ...\n})\n\n// Create mapper\nexport const fooMapper = createMapper(foo)\n```\n\n```ts\nimport Vue from 'vue'\n\n// Import foo mapper\nimport { fooMapper } from '@/store/modules/foo'\n\nexport default Vue.extend({\n  computed: fooMapper.mapGetters(['double']),\n\n  methods: fooMapper.mapActions({\n    incAsync: 'incrementAsync'\n  }),\n\n  created() {\n    console.log(this.double)\n    this.incAsync(undefined)\n  }\n})\n```\n\n### Composable Function\n\nIf you prefer composition api for binding a store module to a component, you can create a composable function by using `createComposable`.\n\n```ts\n// @/store/modules/foo\nimport { Module, createComposable } from 'vuex-smart-module'\n\n// Create module\nexport const foo = new Module({\n  // ...\n})\n\n// Create composable function\nexport const useFoo = createComposable(foo)\n```\n\n```ts\nimport { defineComponent } from '@vue/composition-api'\n\n// Import useFoo\nimport { useFoo } from '@/store/modules/foo'\n\nexport default defineComponent({\n  setup() {\n    // Get Foo module's context\n    const foo = useFoo()\n\n    console.log(foo.getters.double)\n    foo.dispatch('incrementAsync')\n  }\n})\n```\n\n### Method Style Access for Actions and Mutations\n\n`this` in an action and a module context have `actions` and `mutations` properties. They contains module actions and mutations in method form. You can use them instead of `dispatch` or `commit` if you prefer method call style over event emitter style.\n\nThe method style has several advantages: you can use _Go to definition_ for your actions and mutations and it prints simple and easier to understand errors if you pass a wrong payload type, for example.\n\nExample usage in an action:\n\n```ts\nimport { Actions } from 'vuex-smart-module'\n\nclass FooActions extends Actions\u003cFooState, FooGetters, FooMutations, FooActions\u003e {\n  increment(amount: number)\n    // Call `increment` mutation\n    this.mutations.increment(payload)\n  }\n}\n```\n\nExample usage via a context:\n\n```ts\nimport Vue from 'vue'\n\n// Import foo module\nimport { foo } from '@/store/modules/foo'\n\nexport default Vue.extend({\n  mounted() {\n    const ctx = foo.context(this.$store)\n\n    // Call `increment` action\n    ctx.actions.increment(1)\n  }\n})\n```\n\n### Using in Nuxt's Modules Mode\n\nYou can use `Module#getStoreOptions()` method to use vuex-smart-module in [Nuxt's module mode](https://nuxtjs.org/guide/vuex-store).\n\nWhen you have a counter module like the below:\n\n```ts\n// store/counter.ts\nimport { Getters, Actions, Mutations, Module } from 'vuex-smart-module'\n\nexport class CounterState {\n  count = 0\n}\n\nexport class CounterGetters extends Getters\u003cCounterState\u003e {\n  get double() {\n    return this.state.count * 2\n  }\n}\n\nexport class CounterMutations extends Mutations\u003cCounterState\u003e {\n  inc() {\n    this.state.count++\n  }\n}\n\nexport class CounterActions extends Actions\u003cCounterState, CounterGetters, CounterMutations\u003e {\n  inc() {\n    this.commit('inc')\n  }\n}\n\nexport default new Module({\n  state: CounterState,\n  getters: CounterGetters,\n  mutations: CounterMutations,\n  actions: CounterActions\n})\n```\n\nConstruct a vuex-smart-module root module and export the store options acquired with `getStoreOptions` in `store/index.ts`.\nNote that you have to register all nested modules through the root module:\n\n```ts\n// store/index.ts\nimport { Module } from 'vuex-smart-module'\nimport counter from './counter'\n\nconst root = new Module({\n  modules: {\n    counter\n  }\n})\n\nexport const {\n  state,\n  getters,\n  mutations,\n  actions,\n  modules,\n  plugins\n} = root.getStoreOptions()\n```\n\nIf you want to extend a store option, you can manually modify it:\n\n```ts\n// store/index.ts\nconst options = root.getStoreOptions()\n\nexport const {\n  state,\n  getters,\n  mutations,\n  actions,\n  modules\n} = options\n\n// Add an extra plugin\nexport const plugins = options.plugins.concat([otherPlugin])\n```\n\n### Hot Module Replacement\n\nTo utilize [hot module replacement](https://webpack.js.org/concepts/hot-module-replacement/) for the store created with vuex-smart-module, we provide `hotUpdate` function.\n\nThe below is an example how to use `hotUpdate` function:\n\n```ts\nimport { createStore, hotUpdate } from 'vuex-smart-module'\nimport root from './root'\n\nexport const store = createStore(root)\n\nif (module.hot) {\n  // accept actions and mutations as hot modules\n  module.hot.accept(['./root'], () =\u003e {\n    // require the updated modules\n    // have to add .default here\n    const newRoot = require('./root').default\n\n    // swap in the new root module by using `hotUpdate` provided from vuex-smart-module.\n    hotUpdate(store, newRoot)\n  })\n}\n```\n\nNote that you cannot use `hotUpdate` under Vuex store instance. Use `hotUpdate` function imported from `vuex-smart-module`.\n\n## Testing\n\n### Unit testing getters, mutations and actions\n\nvuex-smart-module provides the `inject` helper function which allows you to inject mock dependencies into getters, mutations and actions instances. You can inject any properties for test:\n\n```ts\nimport { inject } from 'vuex-smart-module'\nimport { FooGetters, FooActions } from '@/store/modules/foo'\n\nit('returns doubled value', () =\u003e {\n  // Inject mock state into getters\n  const getters = inject(FooGetters, {\n    state: {\n      count: 5\n    }\n  })\n\n  // Test double getter\n  expect(getters.double).toBe(10)\n})\n\nit('increments asynchronously', async () =\u003e {\n  // Inject mock commit method\n  const commit = jest.fn()\n  const actions = inject(FooActions, {\n    commit\n  })\n\n  await actions.incrementAsync({\n    amount: 3\n    interval: 1\n  })\n\n  // Check mock commit method is called\n  expect(commit).toHaveBeenCalledWith('increment', 3)\n})\n```\n\n### Mocking modules to test components\n\nWhen you want to mock some module assets, you can directly inject a mock constructor into the module options. For example, you will test the following component which is using the `counter` module:\n\n```vue\n\u003ctemplate\u003e\n  \u003cbutton @click=\"increment\"\u003eIncrement\u003c/button\u003e\n\u003c/template\u003e\n\n\u003cscript lang=\"ts\"\u003e\nimport Vue from 'vue'\n\n// use counter Mapper\nimport { counterMapper } from '@/store/modules/counter'\n\nexport default Vue.extend({\n  methods: counterMapper.mapMutations(['increment'])\n})\n\u003c/script\u003e\n```\n\nIn the spec file, mock the `mutations` option in the `counter` module. The below is a [Jest](https://jestjs.io/) example but the essential idea holds true for many test frameworks:\n\n```ts\nimport * as Vuex from 'vuex'\nimport { shallowMount, createLocalVue } from '@vue/test-utils'\nimport { createStore } from 'vuex-smart-module'\n\n// component which we want to test\nimport Counter from '@/components/Counter.vue'\n\n// counter module which we want to mock\nimport counter, { CounterMutations } from '@/store/modules/counter'\n\nconst localVue = createLocalVue()\nlocalVue.use(Vuex)\n\n// make sure that you clean mocked object after each test case\nconst originalMutations = counter.options.mutations\nafterEach(() =\u003e {\n  counter.options.mutations = originalMutations\n})\n\nit('calls increment mutation', () =\u003e {\n  // create spy\n  const spy = jest.fn()\n\n  // create mock mutation\n  class MockMutations extends CounterMutations {\n    // override increment method for mock\n    increment() {\n      spy()\n    }\n  }\n\n  // inject mock\n  counter.options.mutations = MockMutations\n\n  // create mock store\n  const store = createStore(counter)\n\n  // emulate click event\n  shallowMount(Counter, { store, localVue }).trigger('click')\n\n  // check the mock function was called\n  expect(spy).toHaveBeenCalled()\n})\n```\n\n### Mocking nested modules and dependencies\n\nUsing dependencies and nested module contexts in Actions requires to mock them in tests.\n\nSo you test the following Actions class that has been constructed as described in the section above:\n\n```ts\nimport { Store } from 'vuex'\nimport { Actions } from 'vuex-smart-module'\n\nclass FooActions extends Actions {\n  // Declare dependency type\n  store!: Store\u003cFooState\u003e\n  bar!: Context\u003ctypeof bar\u003e\n\n  // Called after the module is initialized\n  $init(store: Store\u003cFooState\u003e): void {\n    // Retain store instance for later\n    this.store = store\n    this.bar = bar.context(store)\n  }\n\n  async fetch(): Promise\u003cvoid\u003e {\n    console.log(await this.store.$axios.$get('...'))\n    this.bar.dispatch(...)\n  }\n}\n```\n\nThen the Jest spec file would be written as:\n\n```ts\nimport { inject } from 'vuex-smart-module'\nimport { FooActions } from '@/store/modules/foo'\n\ndescribe('FooActions', () =\u003e {\n  it('calls the dependency and dispatches the remote action', async () =\u003e {\n    const axiosGet = jest.fn()\n    const barDispatch = jest.fn()\n\n    const actions = inject(FooActions, {\n      store: {\n        $axios: {\n          $get: axiosGet\n        }\n      },\n\n      bar: {\n        dispatch: barDispatch\n      }\n    })\n\n    await actions.fetch()\n\n    expect(axiosGet).toHaveBeenCalledWith(...)\n    expect(barDispatch).toHaveBeenCalledWith(...)\n  })\n})\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fktsn%2Fvuex-smart-module","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fktsn%2Fvuex-smart-module","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fktsn%2Fvuex-smart-module/lists"}