quasarframework / quasar

Quasar Framework - Build high-performance VueJS user interfaces in record time
https://quasar.dev
MIT License
25.95k stars 3.52k forks source link

[SSR] quasar/wrappers `store` type is incompatible with `vuex-module-decorator` #7629

Closed Nigui closed 3 years ago

Nigui commented 4 years ago

Describe the bug

I'm working with the following tools :

quasar/wrappers store function returns StoreCallback (which is a shorthand for (params: StoreParams) => Store<any>)

When we declare a new @Module with vuex-module-decorator we have to pass a reference to vuex store which must be of type Store<any>.

(params: StoreParams) => Store<any> is explicitly not equal to Store<any> so typescript throws the following error

ERROR in src/store/example.ts(4,2):
TS1238: Unable to resolve signature of class decorator when called as an expression.
  This expression is not callable.
    Type 'void' has no call signatures.
ERROR in src/store/example.ts(4,2):
TS2769: No overload matches this call.
  Overload 1 of 2, '(module: Function & Module<unknown, any>): void', gave the following error.
    Argument of type '{ dynamic: boolean; name: string; namespaced: true; store: StoreCallback; }' is not assignable to parameter of type 'Function & Module<unknown, any>'.
      Object literal may only specify known properties, and 'dynamic' does not exist in type 'Function & Module<unknown, any>'.
  Overload 2 of 2, '(options: ModuleOptions): ClassDecorator', gave the following error.
    Type 'StoreCallback' is missing the following properties from type 'Store<any>': state, getters, replaceState, dispatch, and 8 more.
Version: typescript 3.9.5, eslint 6.8.0

My workaround

I force store return type to Store<any> in @/store/index.ts

export default (store(
    ({ Vue }): Store<any> => {
        Vue.use(Vuex);

        return new Vuex.Store<any>({
            modules: {
                // example
            },

            // enable strict mode (adds overhead!)
            // for dev mode only
            strict: !!process.env.DEV,
        });
    },
) as unknown) as Store<StateInterface>;

To Reproduce Craft a sample with my introduced stack

Expected behavior I would like to use my (not very unusual ) stack without any typescript errors

IlCallo commented 4 years ago

Hi @Nigui, I have no experiece with vuex-module-decorator, but I don't see anywhere @Module in the code you provided. The typing of the store wrapper is correct and casting it to something else will probably cause problems.

Note that, if you need the store instance, you can just move the Vuex.Store initialization outside the function and export it as a named export (which IS DIFFERENT from the default export, which is a function returning the store instance).

Vue.use(Vuex);

export const store = new Vuex.Store<any>({
  modules: {
    // example
  },

  // enable strict mode (adds overhead!)
  // for dev mode only
  strict: !!process.env.DEV,
});

export default store(({ Vue }) => {
        return store
    },
);
seanaye commented 3 years ago

I tried the above workaround in SSR mode and I get [vuex] must call Vue.use(Vuex) before creating a store instance. EDIT: I thought calling Vue.use(Vuex) outside a function would cause state pollution on ssr, it doesnt. You can just put the line before

IlCallo commented 3 years ago

Updated the example to actually install the plugin before creating the store.

Closing due to lack of answer from issue author

TobyMosque commented 3 years ago

While that isn't a problem in others modes, in the SSR mode you would keep the store and router wrappers, that is needed to prevent poisoning of the global scope. That said, if u wanna to access the store and/or router, u would do this thought a vue component or inside a boot and/or preFetch, other way is passing the store/router as parameter to your "singletons".

TL;DR; anyway, i was able to make the vuex-module-decorators work, all what u need todo is call getModule after register a module.

quasar create ts-store
yarn add -D vuex-module-decorators
# that is needed to generate the needed flags, without this, your code editor will not recognize the store in the wrappers (router, boot, preFetch, etc)
quasar dev -m ssr

in order to test, i created two modules, one will be registered globally and the another on demand. src/store/modules/app.js

import { uid } from 'quasar'
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'

@Module({ namespaced: true, name: 'app' })
export default class App extends VuexModule {
  uid = ''

  @Mutation
  setUid(uid: string) {
    this.uid = uid
  }

  @Action({ commit: 'setUid' })
  async uidAsync() {
    await new Promise(resolve => setTimeout(resolve, 250))
    return uid()
  }

  get reverseUid () {
    return this.uid.split('').reverse().join('')
  }
}

src/store/modules/ondemand.js

import { uid } from 'quasar'
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'

@Module({ namespaced: true, name: 'demand' })
export default class App extends VuexModule {
  uid = ''

  @Mutation
  setUid(uid: string) {
    this.uid = uid
  }

  @Action({ commit: 'setUid' })
  async uidAsync() {
    await new Promise(resolve => setTimeout(resolve, 250))
    return uid()
  }

  get reverseUid () {
    return this.uid.split('').reverse().join('')
  }
}

and now, the store/index.js, who is where the magic happens:

import { store as wrapper } from 'quasar/wrappers'
import Vuex, { Store } from 'vuex'
import { getModule } from 'vuex-module-decorators'

import app from './modules/app';

export interface StateInterface {
  app: unknown;
  demand: unknown;
}

export default wrapper(function ({ Vue }) {
  Vue.use(Vuex)

  const store = new Store<StateInterface>({
    modules: {
      app
    },

    strict: !!process.env.DEBUGGING
  })

  // this line does all the magic
  getModule(app, store)
  return store
})

now, try to dispatch an action from a boot: src/boot/store.js

import { boot } from 'quasar/wrappers'
import { Store } from 'vuex'
import { StateInterface } from 'src/store'

export default boot<Store<StateInterface>>(({ store }) => {
  return store.dispatch('app/uidAsync')
})

don't forget to register this boot to run only at the server side: quasar.config.js

module.exports = configure(function (ctx) {
  return {
    boot: [
      'composition-api',
      { path: 'store', client: false }
    ],
  }
}

And finally, the last test, lets register the ondemand module inside the index page (using the Store Code Splitting strategy)

<template>
  <q-page class="row items-center justify-evenly">
    <div class="row">
      <div class="col col-auto">App UID:</div>
      <div class="col">{{appUid}}</div>
    </div>
    <div class="row">
      <div class="col col-auto">Page UID:</div>
      <div class="col">{{demandUid}}</div>
    </div>
    <div class="row">
      <div class="col col-auto">Reversed App UID:</div>
      <div class="col">{{reversedAppUid}}</div>
    </div>
    <div class="row">
      <div class="col col-auto">Reversed Page UID:</div>
      <div class="col">{{reversedDemandUid}}</div>
    </div>
  </q-page>
</template>
import { defineComponent } from '@vue/composition-api'
import { getModule } from 'vuex-module-decorators'
import { preFetch } from 'quasar/wrappers'
import { StateInterface } from 'src/store'
import { Store, ModuleOptions } from 'vuex'
import OnDemand from 'src/store/modules/ondemand'

function registerModule(store: Store<StateInterface>, options?: ModuleOptions | undefined) {
  if (!store.hasModule('demand')) {
    store.registerModule('demand', OnDemand, options)
    // this line does all the magic
    getModule(OnDemand, store)
  }
}

export default defineComponent({
  name: 'PageIndex',
  preFetch: preFetch<Store<StateInterface>>(({ store }) => {
    registerModule(store)
    return store.dispatch('demand/uidAsync')
  }),
  created () {
    registerModule(this.$store, { preserveState: true })
  },
  beforeDestroy () {
    this.$store.unregisterModule('demand')
  },
  computed: {
    appUid () {
      return this.$store.state.app.uid
    },
    demandUid () {
      return this.$store.state.demand.uid
    },
    reversedAppUid () {
      return this.$store.getters['app/reverseUid']
    },
    reversedDemandUid () {
      return this.$store.getters['demand/reverseUid']
    }
  }
})

now, run the project and check if anything had run as expected (check if the variables present in the window.__INITIAL_STATE__ are equals to the one displayed at the screen) app

Download the demo project here