Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to split store in different files? And the limits of object type inference #802

Closed
nicolidin opened this issue Nov 16, 2021 · 46 comments
Labels
contribution welcome 📚 docs Related to documentation changes

Comments

@nicolidin
Copy link

nicolidin commented Nov 16, 2021

Hello,

I would simply like to split a store in different files and especially to make my different store parts (state, getter, actions) able to inherit from interface, abstract classes, etc..
Furthermore, splitting parts of a Pinia store seems to break the this usage.
As Pinia seems to not really be typed (cause it is only based on object type inference), we just can't split the different parts of a store.

I really wonder why Pinia didn't do like vuex-smart-module allowing to have strong typing through classes usage and to use directly the type of each parts of a store without creating redundant Interface types.
People easily say, "oh classes usage like in vuex-smart-module is more verbose cause you have to specify generics", but when you have to use State, Getter or Action TYPE, you clearly don't want to write/specify n interfaces with k methods for each part a store.

It is common for a lot of projects with strong architecture to have for requirement the need of an abstraction between different parts of different stores.
Example two instances of store like:
ProjectResourceStore and HomeResourceStore having each one Action inheriting from the same abstract class AResourceActions.
What is really useful is that AResourceActions allows having default implementations and specifying a "contract".
ProjectResourceAction and HomeResourceAction will be obligated to implement all non-implemented AResourceActions methods.

What I say above seems to be clearly impossible with Pinia as it has only object type inference.

It's also important to understand that with classes if I want to have a Type of a store part (State, Getter, Action), I don't have to manually specify all the properties/methods in a redundant Interface that will be used as a way to have non-infered type.
Indeed, with classes, I can directly use the class as a Type.

This problem has already been mentioned here: #343, but my issue just try to expose the problem with other related issues and limitations of Pinia type system.
Also, no satisfying code example in the original issue has been provided.

@posva posva added contribution welcome 📚 docs Related to documentation changes and removed feature request labels Nov 17, 2021
@posva
Copy link
Member

posva commented Nov 17, 2021

It would be nice to document how to split the store into multiple files or functions when needed but the problem has always been mentioned without code samples. It is crucial that real-world examples are provided to better understand what kind of splitting is needed.

So far I identified these possibilities:

  • Splitting actions or getters into functions that receive everything needed as parameters (Store type or ReturnType<typeof useStore> could be useful)
  • Splitting a store into smaller stores
  • Using a setup store to split the store into smaller functions that are composables

@posva
Copy link
Member

posva commented Nov 17, 2021

Regarding the class types, that's a completely different paradigm and, like vuex-smart-module, it should exist in a separate package. I don't use vuex-smart-module but you seem to like it very much, you should write a similar plugin for pinia!

@posva posva mentioned this issue Nov 28, 2021
20 tasks
@exbotanical
Copy link

It would be nice to document how to split the store into multiple files

This is a considerably prevalent design pattern (using Vuex 4 currently) for my team, and we'd like to continue using it.

So far I identified these possibilities:

  • Splitting actions or getters into functions that receive everything needed as parameters (Store type or ReturnType<typeof useStore> could be useful)
  • Splitting a store into smaller stores
  • Using a setup store to split the store into smaller functions that are composables

@posva Have you had the chance to try any of these options you've listed? I'm just curious if anyone has figured out a clean way to implement this before I deep dive this weekend. I'd be happy to (attempt to) contribute if supporting this pattern is indeed a direction you'd like to go in with Pinia.

In any case, I'm super impressed with Pinia and look forward to adopting it across our projects over the next year!

@rzym-on
Copy link

rzym-on commented Dec 25, 2021

Maybe I'll put something, that will be usefull to this discussion. So far I came across with this example that is working well for me with pinia store.

It is crucial that real-world examples are provided to better understand what kind of splitting is needed.

And according to this, I provided a real-world example. I have some common CRUD getters and actions that I want to use in multiple stores.

baseCrud.ts file:

import axios from 'axios';

export interface CrudState {
  loading: boolean;
  items: any[];
  apiUrl: string;
}

export function crudState(apiCollectionUrl: string) {
  return (): CrudState => ({
    loading: false,
    items: [],
    apiUrl: `http://localhost:8080/api/${apiCollectionUrl}`,
  });
}

export function crudGetters() {
  return {
    allItems: (state:CrudState) => state.items,
    getById():any {
      return (id) => this.allItems.find((x) => x.id === id);
    },
  };
}

export function crudActions() {
  return {
    async initStore() {
      this.loading = true;
      const { data } = await axios.get(this.apiUrl);
      this.items = data;
      this.loading = false;
    },
    async clearAndInitStore() {
      this.items = [];
      await this.initStore();
    },
  };
}

taskStore.ts file in same directory:

import { defineStore } from 'pinia';
import { crudState, crudGetters, crudActions } from './baseCrud';

export default defineStore('tasks', {
  state: crudState('tasks/'),
  getters: {
    ...crudGetters(),
    // Your additional getters
    activeTasks: (state) => state.items.filter((task) => !task.done),
    allItemsCount(): number {
      return this.allItems.length;
    },
  },
  actions: {
    ...crudActions(),
    // Your additional actions
  },
});

using taskStore in main.ts file in same directory:

import useTaskStore from './taskStore';

// Autocompletion works fine here
const taskStore = useTaskStore();
taskStore.initStore();
const count = taskStore.allItemsCount;
const task = taskStore.getById(1);

@posva
Copy link
Member

posva commented Dec 28, 2021

This was added to #829

@aparajita
Copy link

aparajita commented Jan 21, 2022

Just a followup on this. The example provided by @rzym-on may run, but it fails under strict type checking. After a massive amount of sleuthing into the pinia types, I finally figured out how to split up a store into multiple files and preserve complete type checking. Here we go!

First you need to add this utility somewhere in your project. Let's assume it's in @store/:

extractStore.ts

import type {
  PiniaCustomStateProperties,
  StoreActions,
  StoreGeneric,
  StoreGetters,
  StoreState
} from 'pinia'
import type { ToRefs } from 'vue'
import { isReactive, isRef, toRaw, toRef } from 'vue'

type Extracted<SS> = ToRefs<
  StoreState<SS> & StoreGetters<SS> & PiniaCustomStateProperties<StoreState<SS>>
> &
  StoreActions<SS>

/**
 * Creates an object of references with all the state, getters, actions
 * and plugin-added state properties of the store.
 *
 * @param store - store to extract the refs from
 */
export function extractStore<SS extends StoreGeneric>(store: SS): Extracted<SS> {
  const rawStore = toRaw(store)
  const refs: Record<string, unknown> = {}

  for (const [key, value] of Object.entries(rawStore)) {
    if (isRef(value) || isReactive(value)) {
      refs[key] = toRef(store, key)
    } else if (typeof value === 'function') {
      refs[key] = value
    }
  }

  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  return refs as Extracted<SS>
}

Now create a directory for the store. Let's assume it's @store/foo. These files go in the directory:

state.ts

export interface State {
  foo: string
  bar: number
}

export const useState = defineStore({
  id: 'repo.state',

  state: (): State => {
    return {
      foo: 'bar',
      bar: 7
    }
  }
})

getters.ts

import { computed } from 'vue'
import { useState } from './state'

export const useGetters = defineStore('repo.getters', () => {
  const state = useState()

  const foobar = computed((): string => {
    return `foo-${state.foo}`
  })

  const doubleBar = computed((): string => {
    return state.bar * 2
  })

  return {
    foobar,
    doubleBar
  }
})

actions.ts

import { useState } from './state'

export const useActions = defineStore('repo.actions', () => {
  const state = useState()

  function alertFoo(): void {
    alert(state.foo)
  }

  function incrementBar(amount = 1) {
    state.bar += amount
  }

  // Note you are free to define as many internal functions as you want.
  // You only expose the functions that are returned.
  return {
    alerttFoo,
    incrementBar
  }
})

Now you can bring all of the pieces together like this:

import { extractStore } from '@store/extractStore'
import { defineStore } from 'pinia'
import { useActions } from './actions'
import { useGetters } from './getters'
import { useState } from './state'

export const useFooStore = defineStore('foo', () => {
  return {
    ...extractStore(useState()),
    ...extractStore(useGetters()),
    ...extractStore(useActions())
  }
})

Boom! Completely typed and nicely factored. You would then import it like this:

import { useFooStore } from '@store/foo'

const store = useFooStore()
let foo = store.foo // 'bar'
foo = store.foobar // 'foo-bar'
let bar = store.doubleBar // 14
store.incrementBar(3) // store.bar === 10
bar = store.doubleBar // 20

Enjoy! 😁

@Grawl
Copy link

Grawl commented Feb 3, 2022

@aparajita thank you! but how to access getters inside of actions?

@aparajita
Copy link

@Grawl the getters are just a regular store with access to your state.

actions.ts

import { useGetters } from './getters'
import { useState } from ['./state']()

export const useActions = defineStore('repo.actions', () => {
  const state = useState()
  const getters = useGetters()

  function alertFoo(): void {
    alert(getters.doubleBar)
  }
}

@mahmoudsaeed
Copy link

@aparajita It sounds like an anti-pattern to define different stores for state, actions and getters.

@nicolidin
Copy link
Author

Yes @mahmoudsaeed, I think the same it is really hacky and a bad pattern, hope there will be a real fix.

@aparajita
Copy link

@mahmoudsaeed Thanks for stating the obvious. Feel free to find a more elegant workaround.

@aparajita
Copy link

@mahmoudsaeed and @nicolidin, I should add that I was actually following @posva's earlier suggestion to split up a store into smaller stores, so if it's an anti-pattern you can blame him! I was unable to get his other suggestions to work with strict type checking — it requires some serious TypeScript ninja skills because of the way pinia infers types.

Obviously it would be nice if we had an easier way to split up stores.

@mahmoudsaeed
Copy link

mahmoudsaeed commented Feb 17, 2022

@aparajita I think @posva meant to say splitting up a store into smaller single-file stores.

@aparajita
Copy link

As I said before, if you can figure out a way to split up a store that is completely type safe and retains type inference, please do. Since you have the time to criticize others, maybe you would be kind enough to spend the time and effort to come up with a better solution.

@mahmoudsaeed
Copy link

@aparajita Please don't take it personally. I appreciate your solution.

@aparajita
Copy link

@mahmoudsaeed I am just as eager as you to have an official solution that is not a hack.

@fgarit-te
Copy link

@aparajita I'm with you. I am thinking of migrating to Pinia but would rather not until there is an official way to break a store into modules and keep good type safety.

@aparajita
Copy link

@fgarit-te Then bug @posva to show us how — or make whatever changes are necessary to make it possible.

Note that my technique, though a bit of a hack, is quite clean and maintains complete type safety. The benefits of using Pinia are worth it.

@fgarit
Copy link

fgarit commented Feb 23, 2022

Re: well actually, the following is pretty typesafe.

src/stores/useSomeNumberStore.ts

import { defineStore, Store } from 'pinia';

/* Defining our store types */

type State = {
    someNumber: number;
};

type Getters = {
    someNumberPlusOne: number;
    someNumberPlusOneTimesTwo: number;
};

type Actions = {
    setSomeNumber: (newNumber: number) => Promise<void>;
    wrappedSetSomeNumber: (newNumber: number) => Promise<void>;
};

type SomeNumberStore = Store<'someNumber', State, Getters, Actions>;

/* Now defining the store one element at a time */

const getters: PiniaGetterAdaptor<Getters, SomeNumberStore> = {
    someNumberPlusOne(state) {
        return state.someNumber + 1;
    },
    someNumberPlusOneTimesTwo() {
        return this.someNumberPlusOne * 2;
    },
};

const actions: PiniaActionAdaptor<Actions, SomeNumberStore> = {
    async setSomeNumber(newNumber: number) {
        this.someNumber = newNumber;
    },
    async wrappedSetSomeNumber(newNumber: number) {
        this.setSomeNumber(newNumber);
    },
};

export default defineStore('someNumber', {
    state: getState,
    getters,
    actions,
});

function getState(): State {
    return {
        someNumber: 12,
    };
}

/** Some type helpers */

type PiniaActionAdaptor<
    Type extends Record<string, (...args: any) => any>,
    StoreType extends Store,
> = {
    [Key in keyof Type]: (this: StoreType, ...p: Parameters<Type[Key]>) => ReturnType<Type[Key]>;
};

type PiniaGetterAdaptor<GettersType, StoreType extends Store> = {
    [Key in keyof GettersType]: (this: StoreType, state: StoreType['$state']) => GettersType[Key];
};

src/App.vue

<template>
    <div>someNumberPlusOnTimesTwoDividedByThree: {{ someNumberPlusOnTimesTwoDividedByThree }}</div>
    <div>
        <button @click="setRandomNumber">setRandomNumber</button>
    </div>
</template>

<script lang="ts">
import { computed, defineComponent } from 'vue';
import useSomeNumberStore from './stores/useSomeNumberStore';

export default defineComponent({
    setup() {
        const someNumberStore = useSomeNumberStore();
        const someNumberPlusOnTimesTwoDividedByThree = computed(
            () => someNumberStore.someNumberPlusOneTimesTwo / 3,
        );
        return {
            someNumberPlusOnTimesTwoDividedByThree,
            setRandomNumber,
        };

        function setRandomNumber() {
            someNumberStore.wrappedSetSomeNumber(Math.round(Math.random() * 1000));
        }
    },
});
</script>

src/main.ts

import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

@aparajita
Copy link

@fgarit Brilliant! I knew it was just a matter of finding someone who was better at TypeScript than me. 😁

It's almost working for me. I successfully split the state, getters and actions into separate files, but I'm getting an error in const actions: PiniaActionAdaptor<Actions, SomeStore>. TS is complaining about Actions:

TS2344: Type 'Actions' does not satisfy the constraint 'Record<string, (...args: any) => any>'.
Index signature for type 'string' is missing in type 'Actions'.

Are you not getting that error? If not, it may be a difference in our tsconfig. Here's the relevant part of mine:

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "declaration": false,
    "declarationMap": false,
    "esModuleInterop": true,
    "exactOptionalPropertyTypes": true,
    "importsNotUsedAsValues": "error",
    "module": "esnext",
    "moduleResolution": "node",
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": false,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": false,
    "noUnusedParameters": true,
    "pretty": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "esnext",
    "types": [
      "node"
    ],
    "useDefineForClassFields": true,
    "useUnknownInCatchVariables": true,
    "lib": ["esnext", "dom"],
}

@fgarit
Copy link

fgarit commented Feb 23, 2022

@aparajita I tested with your tsconfig and it's fine but judging from the error you get, it's the way your Actions type is setup, it should be something like:

type Actions = {
    someActionName: (params: any) => any;
}

@aparajita
Copy link

@fgarit Okay, the issue was my @typescript-eslint rules change object type declarations into interfaces. Changing it back to a type removed the Actions error.

This is a very interesting behavior of TypeScript; apparently TS requires an interface which extends Record<string, any> to have a [string: any] index signature, but does NOT for the equivalent type.

However, I'm now getting a very deep TS error with my code, which is a much more complex example. I'm not going to ask you to debug it, so unfortunately I'll have to abandon your approach.

@aislanmaia
Copy link

If doesn't exist an official solution for this, why this issue is closed ?

I'm trying to port a big frontend application and the first thing is to split the huge state/actions modules and is a disappointment to see a poor doc for the new official state management of Vue community.

Coming from other libs and communities, I see Vue (and its libraries) is lacking behind in the documentation. Just recently the docs was revamped for Vue 3.

Back to the problem, I have solution to splitting problem, but the typing isn't 100% accurate, because my types like:

filterGroups: FilterGroupModel[] in a splitted state file is now infereced into an actions file, for example: FilterState.filterGroups: {id: number, groupName: string, groupTag: string, etc... when it should see the FilterGroupModel type there

@fgarit
Copy link

fgarit commented Mar 1, 2022

Back to the problem, I have solution to splitting problem, but the typing isn't 100% accurate, because my types like:

filterGroups: FilterGroupModel[] in a splitted state file is now infereced into an actions file, for example: FilterState.filterGroups: {id: number, groupName: string, groupTag: string, etc... when it should see the FilterGroupModel type there

What solution did you use for typing? I have had the same issue btw with types loosing their original reference.

@aislanmaia
Copy link

Back to the problem, I have solution to splitting problem, but the typing isn't 100% accurate, because my types like:
filterGroups: FilterGroupModel[] in a splitted state file is now infereced into an actions file, for example: FilterState.filterGroups: {id: number, groupName: string, groupTag: string, etc... when it should see the FilterGroupModel type there

What solution did you use for typing? I have had the same issue btw with types loosing their original reference.

That's the actual problem. Splitting is easy, but my types lost their reference.

@aparajita
Copy link

@fgarit @aislanmaia If it's the problem I think you're talking about, this is a known problem. For reference:

#491
#871

@aislanmaia
Copy link

@fgarit @aislanmaia If it's the problem I think you're talking about, this is a known problem. For reference:

#491 #871

In other words I need, for now, to force typing by casting for every state usage?

@aparajita
Copy link

In other words I need, for now, to force typing by casting for every state usage?

Not if you use my technique.

@diadal
Copy link

diadal commented Mar 15, 2022

Just a followup on this. The example provided by @rzym-on may run, but it fails under strict type checking. After a massive amount of sleuthing into the pinia types, I finally figured out how to split up a store into multiple files and preserve complete type checking. Here we go!

First you need to add this utility somewhere in your project. Let's assume it's in @store/:

extractStore.ts

import type {
  PiniaCustomStateProperties,
  StoreActions,
  StoreGeneric,
  StoreGetters,
  StoreState
} from 'pinia'
import type { ToRefs } from 'vue'
import { isReactive, isRef, toRaw, toRef } from 'vue'

type Extracted<SS> = ToRefs<
  StoreState<SS> & StoreGetters<SS> & PiniaCustomStateProperties<StoreState<SS>>
> &
  StoreActions<SS>

/**
 * Creates an object of references with all the state, getters, actions
 * and plugin-added state properties of the store.
 *
 * @param store - store to extract the refs from
 */
export function extractStore<SS extends StoreGeneric>(store: SS): Extracted<SS> {
  const rawStore = toRaw(store)
  const refs: Record<string, unknown> = {}

  for (const [key, value] of Object.entries(rawStore)) {
    if (isRef(value) || isReactive(value)) {
      refs[key] = toRef(store, key)
    } else if (typeof value === 'function') {
      refs[key] = value
    }
  }

  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  return refs as Extracted<SS>
}

Now create a directory for the store. Let's assume it's @store/foo. These files go in the directory:

state.ts

export interface State {
  foo: string
  bar: number
}

export const useState = defineStore({
  id: 'repo.state',

  state: (): State => {
    return {
      foo: 'bar',
      bar: 7
    }
  }
})

getters.ts

import { computed } from 'vue'
import { useState } from './state'

export const useGetters = defineStore('repo.getters', () => {
  const state = useState()

  const foobar = computed((): string => {
    return `foo-${state.foo}`
  })

  const doubleBar = computed((): string => {
    return state.bar * 2
  })

  return {
    foobar,
    doubleBar
  }
})

actions.ts

import { useState } from './state'

export const useActions = defineStore('repo.actions', () => {
  const state = useState()

  function alertFoo(): void {
    alert(state.foo)
  }

  function incrementBar(amount = 1) {
    state.bar += amount
  }

  // Note you are free to define as many internal functions as you want.
  // You only expose the functions that are returned.
  return {
    alerttFoo,
    incrementBar
  }
})

Now you can bring all of the pieces together like this:

import { extractStore } from '@store/extractStore'
import { defineStore } from 'pinia'
import { useActions } from './actions'
import { useGetters } from './getters'
import { useState } from './state'

export const useFooStore = defineStore('foo', () => {
  return {
    ...extractStore(useState()),
    ...extractStore(useGetters()),
    ...extractStore(useActions())
  }
})

Boom! Completely typed and nicely factored. You would then import it like this:

import { useFooStore } from '@store/foo'

const store = useFooStore()
let foo = store.foo // 'bar'
foo = store.foobar // 'foo-bar'
let bar = store.doubleBar // 14
store.incrementBar(3) // store.bar === 10
bar = store.doubleBar // 20

Enjoy! 😁

best solution

@aparajita
Copy link

Thanks @diadal. I truly wish there were a better solution, but pinia depends heavily on very complex type inference, and no other solution I have seen so far works with complex stores.

@alexisjamet
Copy link

alexisjamet commented Apr 15, 2022

Thanks for the tips above. Here is how I've separated each actions, getters, state in files while keeping types:

Folder /stores

File /index.ts

A single file exporting all my stores (I prefer it this way when importing stores in components):

export const useUIStore = defineStore('ui', {
  state: () => uiStoreState,
  actions: uiStoreActions,
})

export const useAuthStore = defineStore('auth', {
  state: () => authStoreState,
  getters: authStoreGetters,
  actions: authStoreActions,
})

File /types.ts

export type PiniaActionAdaptor<
  Type extends Record<string, (...args: any) => any>,
  StoreType extends Store,
> = {
  [Key in keyof Type]: (
    this: StoreType,
    ...p: Parameters<Type[Key]>
  ) => ReturnType<Type[Key]>
}

Folder /auth

File /types.ts

import { Store } from 'pinia'

export interface AuthStoreState {
  isAuthenticated: boolean
  errorMessage: null | string
}

export type AuthStoreActions = {
  signIn: (credentials: Credentials) => Promise<void>
  resetErrorMessage: () => void
  logout: () => Promise<void>
}

export type AuthStoreGetters = {}

export type AuthStore = Store<
  'auth',
  AuthStoreState,
  AuthStoreGetters,
  AuthStoreActions
>

File /state.ts

import { AuthStoreState } from './types'

export const authStoreState: AuthStoreState = {
  isAuthenticated: false,
  errorMessage: null,
}

File /actions.ts

export const authStoreActions: PiniaActionAdaptor<AuthStoreActions, AuthStore> =
  {
    signIn,
    logout,
    resetErrorMessage() {
      this.errorMessage = null
    },
  }

In a dedicated folder called /actions I put all my actions in separate files:
File /logout.ts

import { useUserStore } from '@/stores'
import { AuthStore } from '../types'

export async function logout(this: AuthStore) {
  this.isAuthenticated = false
  useUserStore().setUser(null)
  router.push({ name: RoutesNames.Home })
}

@tunngtv
Copy link

tunngtv commented Apr 15, 2022

hi @alexisjamet
I have seen your code snippets, but I still don't know how to use actions in state ? and using getters, can you share clearer code snippets? thanks

@diadal
Copy link

diadal commented Apr 15, 2022

@tunngtv you can create a plugin call it helper or whatever

take note of app.use(createPinia()); & app.config.globalProperties.$auth = helper; you can formulate yours using below snippet after creating your store


import { Helper, Erre, Freg, $Logi } from '../components/models/model';
import { Notify } from 'quasar';
import { axiosInstance } from './axios';
import { boot } from 'quasar/wrappers';
import { createPinia } from 'pinia';
import { useAuthStore } from '../store/auth';

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $auth: Helper;
  }
}

export default boot(({ app, router, store }) => {
  app.use(createPinia());

  const storeX = useAuthStore(store);
  axiosInstance.interceptors.response.use(
    (response) => {
      return response;
    },
    async (e) => {
      const error: Erre = <Erre>e;
      if (!error.response) {
        Notify.create({
          message: 'Internet connection error',
          color: 'red',
        });
        return { data: { status: 0 } };
      } else if (error.response && error.response.status === 500) {
        Notify.create({
          color: 'negative',
          textColor: 'white',
          message:
            'Something went wrong refresh and try again or contact admin',
        });
        return { data: { status: 0 } };
      } else if (
        error?.response?.data &&
        (error?.response?.data?.message === 'Unauthenticated.' ||
          error?.response?.status === 401 ||
          error?.response?.status === 419)
      ) {
        const logout = <boolean>await storeX.applogout();
        if (logout) {
          window.location.reload();
        }
      } else if (error?.response?.status === 404) {
        return { data: error?.response?.data };
      }
      return { data: { status: 446 } };
    }
  );

  router.beforeEach(async (to, _from, next) => {
    await fetcher();
    const isLogin = loggedIn();
    const autPath = [
      '/login',
      '/register',
      '/password',
      '/forget-password',
    ].includes(to.path);
    if (to.meta.auth && !isLogin) {
      next('/login');
    } else if (autPath && isLogin) {
      next('/welcome');
    } else {
      next();
    }
  });

  const helper = <Helper>{
    register: {},
    loggedIn: {},
    check: {},
    login: {},
    logout: {},
    passwordForgot: {},
    passwordReset: {},
    updateuser: {},
    firstlogin: {},
    newfetch: {},
    fetch: {},
    user: {},
    userUpdate: {},
  };

  function loggedIn() {
    const loggedIn = storeX.loggedIn;
    return loggedIn;
  }

  function logoutF() {
    const lof = storeX.logout();
    return lof;
  }

  async function fetcher() {
    const fet: boolean = <boolean>storeX.fetch();
    return fet;
  }

  function user() {
    const usr = storeX.user;
    return usr;
  }

  function check(roles: string | string[]) {
    const chk = storeX.check(roles);
    return chk;
  }

  helper.register = async (data) => {
    const reg: number | string | Freg = <number | string | Freg>(
      await storeX.register(data)
    );
    return reg;
  };

  helper.loggedIn = loggedIn;
  helper.check = check;
  helper.logout = logoutF;
  helper.login = async (data) => {
    return <boolean | $Logi>await storeX.login(data);
  };
  helper.updateuser = async (data) => {
    return storeX.updateuser(data);
  };

  helper.firstlogin = async (data) => {
    return <boolean>storeX.firstlogin(data);
  };

  helper.userUpdate = async (data) => {
    return <boolean>storeX.userUpdate(data);
  };
  helper.fetch = fetcher;
  helper.user = user;
  app.config.globalProperties.$auth = helper;
});

@tunngtv
Copy link

tunngtv commented Apr 15, 2022

@diadal
thanks for your answer, but i want to separate the state, action, getters parts in separate files and will import them into the index file, with Vuex it will be easy to do that, i just learned Pinia and everything is fine, I followed the above instructions by @alexisjamet, but I can't seem to access the action, below is my code using

ProfilePage.vue

<template>
    <div class="profile-page">
        <h1>Profile Page</h1>
        Error Message: {{ getErrorMessage }} <br />
        isAuthenticated: {{ isAuthenticated }}
        <button @click="changeMessageToStore">Change Message</button>
    </div>
</template>

<script lang="ts">
import { PageName } from '@/modules/common/constants';
import { defineComponent } from 'vue';
import { useAuthStore } from '../store/index';
import { logout } from '../actions';
// import { AuthStore } from '../constants';

export default defineComponent({
    name: PageName.PROFILE_PAGE,
    setup() {
        const authStore = useAuthStore();
        function changeMessageToStore() {
            useAuthStore.changeMessage();
        }
        const getErrorMessage = authStore.errorMessage;
        const isAuthenticated = authStore.isAuthenticated;
        return {
            getErrorMessage,
            logout,
            isAuthenticated,
            changeMessageToStore,
        };
    },
});
</script>

<style scoped></style>

constants.ts file

import { Store } from 'pinia';

export type PiniaActionAdaptor<
    Type extends Record<string, (...args: any) => any>,
    StoreType extends Store,
> = {
    [Key in keyof Type]: (
        this: StoreType,
        ...p: Parameters<Type[Key]>
    ) => ReturnType<Type[Key]>;
};

export interface AuthStoreState {
    isAuthenticated: boolean;
    errorMessage: null | string;
}

export type AuthStoreActions = {
    changeMessage: () => void;
};

export type AuthStoreGetters = Record<string, unknown>;

export type AuthStore = Store<'auth', AuthStoreState, AuthStoreGetters, AuthStoreActions>;

Folder Store: state.ts file

import { AuthStoreState } from '../constants';

export const authStoreState: AuthStoreState = {
    isAuthenticated: false,
    errorMessage: 'aa',
};

Folder Store: actions.ts file

import { PiniaActionAdaptor, AuthStoreActions, AuthStore } from '../constants';

export const authStoreActions: PiniaActionAdaptor<AuthStoreActions, AuthStore> = {
    changeMessage() {
        this.errorMessage = 'hhh';
    },
};

Folder Store: index.ts file

import { defineStore } from 'pinia';
import { authStoreState } from './state';
import { authStoreActions } from './actions';

export const useAuthStore = defineStore('auth', {
    state: () => authStoreState,
    getters: {},
    actions: {
        ...authStoreActions,
    },
});

i tried creating an actions file outside the store folder, and importing it into SFC vue used to change the state everything seems fine but i don't know how to use actions ?

actions.ts

import { AuthStore } from './constants';

export async function logout(this: AuthStore) {
    this.isAuthenticated = !this.isAuthenticated;
}

Screen Shot 2022-04-16 at 12 20 25 AM

@diadal
Copy link

diadal commented Apr 15, 2022

you can separate it simple

Screen Shot 2022-04-15 at 10 11 11 AM

actions.ts

import { axiosInstance, appClient } from '../../boot/axios';
import { Regs, Sget, User, UserKey } from '../../components/models/model';
import { UserDat } from '../../components/models/acclist';
import { RepUser } from '../../components/models/StoreInterFace';

import { useState } from './state';
import { defineStore } from 'pinia';



const REGISTER_ROUTE = '/register';
const LOGIN_ROUTE = '/login';
const AUTH_LOGOUT = '/logout';


const empty = UserDat;

export const useActions = defineStore('auth.actions', () => {
  const state = useState();

  async function register(data: Regs) {
    try {
      const r = await axiosInstance.post(REGISTER_ROUTE, data);
      return r.data;
    } catch (error) {
     
      return false;
    }
  }

  async function login(data: Regs) {
    try {
      const response: {
        data: { status: boolean; data: { user: User; data: string } };
      } = await axiosInstance.post(LOGIN_ROUTE, data);
      const mainData = response?.data?.data;
      const user = mainData?.user;
      if (user) {
        state.user = user;
        const to: string = mainData.data;
        axiosInstance.defaults.headers.common = {
          'content-type': 'application/json',
          'X-Requested-With': 'XMLHttpRequest',
          'M-Version': process.env.APP_VERSION,
          'M-Client': appClient,
          Authorization: `Bearer ${to}`,
        };
        return <RepUser>{ status: true };
      }
      const rep = <RepUser>(
        (response?.data?.data ? response?.data?.data : response?.data)
      );
      return rep;
    } catch (ee) {
      return false;
    }
  }

  function firstlogin(data: { user: User; data: string }) {
    if (data?.user) {
      const newdata = data.user;
      state.user = newdata;
      const to: string = data.data;
      axiosInstance.defaults.headers.common = {
        'content-type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
        Authorization: `Bearer ${to}`,
      };
      return true;
    }
  }

  function updateuser(data: { tok: string; user: User }) {
    const newdata = data.user;
    state.user = newdata;
    return true;
  }

  function userUpdate(data: { id: string; value: boolean | string }) {
    const updateSate = <UserKey>(<unknown>state.user);
    updateSate[data.id] = data.value;
    return true;
  }

  function fetch() {
  
  }

  async function applogout() {
   
  }

  async function logout() {
    
  }
  return {
    register,
    login,
    updateuser,
    userUpdate,
    firstlogin,
    fetch,
    applogout,
    logout,
  };
});

getters.ts

import { defineStore } from 'pinia';
import { computed } from 'vue';
import { User } from '../../components/models/model';
import { useState } from './state';

export const useGetters = defineStore('auth.getters', () => {
  const state = useState();

  const user = computed((): User => {
    return state.user;
  });

  const loggedIn = computed((): boolean => {
    return state.user.login;
  });

  const check = (roles: string | string[]) => {
    const user = state.user;
    if (user && user.roleNames.length > 0) {
      if (Array.isArray(roles) && roles.length) {
        for (const role of roles) {
          if (!user.roleNames.includes(String(role))) {
            return false;
          }
        }
      } else if (roles) {
        const nrole: string = <string>roles;
        if (!user.roleNames.includes(nrole)) {
          return false;
        }
      }
      return true;
    }
    return false;
  };

  return {
    user,
    loggedIn,
    check,
  };
});

index.ts

import { extractStore } from '../extractStore';
import { defineStore } from 'pinia';
import { useActions } from './actions';
import { useGetters } from './getters';
import { useState } from './state';

export const useAuthStore = defineStore('auth', () => {
  return {
    ...extractStore(useState()),
    ...extractStore(useGetters()),
    ...extractStore(useActions()),
  };
});

state.ts

import { defineStore } from 'pinia';
import { UserDat } from '../../components/models/acclist';
import { State } from '../../components/models/StoreInterFace';

export const useState = defineStore({
  id: 'auth.state',
  state: (): State => {
    return {
      user: UserDat,
    };
  },
});

extractStore.ts

import type {
  PiniaCustomStateProperties,
  StoreActions,
  StoreGeneric,
  StoreGetters,
  StoreState,
} from 'pinia';
import type { ToRefs } from 'vue';
import { isReactive, isRef, toRaw, toRef } from 'vue';

type Extracted<SS> = ToRefs<
  StoreState<SS> & StoreGetters<SS> & PiniaCustomStateProperties<StoreState<SS>>
> &
  StoreActions<SS>;

/**
 * Creates an object of references with all the state, getters, actions
 * and plugin-added state properties of the store.
 *
 * @param store - store to extract the refs from
 */
export function extractStore<SS extends StoreGeneric>(
  store: SS
): Extracted<SS> {
  const rawStore = toRaw(store);
  const refs: Record<string, unknown> = {};

  for (const [key, value] of Object.entries(rawStore)) {
    if (isRef(value) || isReactive(value)) {
      refs[key] = toRef(store, key);
    } else if (typeof value === 'function') {
      refs[key] = value;
    }
  }

  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  return refs as Extracted<SS>;
}

StoreInterFace.ts

/* eslint-disable @typescript-eslint/no-explicit-any */
import { Store } from 'pinia';
import { Freg, Regs, User } from './model';

export type PiniaActionAdaptor<
  Type extends Record<string, (...args: any) => any>,
  StoreType extends Store
> = {
  [Key in keyof Type]: (
    this: StoreType,
    ...p: Parameters<Type[Key]>
  ) => ReturnType<Type[Key]>;
};

export type PiniaGetterAdaptor<GettersType, StoreType extends Store> = {
  [Key in keyof GettersType]: (
    this: StoreType,
    state: StoreType['$state']
  ) => GettersType[Key];
};

export type State = {
  user: User;
};

export type Getters = {
  user: User;
  loggedIn: boolean;
  check: (roles: string | string[]) => boolean;
};

export interface RepUser {
  status: boolean;
  data: { user: User; data: string };
}

export type Actions = {
  register?: (data: Regs) => Promise<number | string | Freg> | boolean;
  loggedIn?: () => boolean;
  check?: (roles: string | string[]) => boolean;
  login?: (data: Regs) => Promise<false | RepUser>;
  logout?: () => Promise<boolean>;
  applogout?: () => boolean;
  passwordForgot?: (data: string[]) => Promise<boolean | Freg>;
  passwordReset?: (data: string[]) => Promise<boolean | Freg>;
  updateuser?: (data: { id: string; user: User }) => boolean;
  userUpdate?: (data: { id: string; value: boolean | string }) => boolean;
  firstlogin?: (data: { user: User; data: string }) => boolean;
  newfetch?: () => Promise<boolean | string | User>;
  fetch?: () => boolean;
  user?: () => User;
};

export type AuthStore = Store<'auth', State, Getters, Actions>;

@diadal
Copy link

diadal commented Apr 15, 2022

@diadal thanks for your answer, but i want to separate the state, action, getters parts in separate files and will import them into the index file, with Vuex it will be easy to do that, i just learned Pinia and everything is fine, I followed the above instructions by @alexisjamet, but I can't seem to access the action, below is my code using

ProfilePage.vue

<template>
    <div class="profile-page">
        <h1>Profile Page</h1>
        Error Message: {{ getErrorMessage }} <br />
        isAuthenticated: {{ isAuthenticated }}
        <button @click="changeMessageToStore">Change Message</button>
    </div>
</template>

<script lang="ts">
import { PageName } from '@/modules/common/constants';
import { defineComponent } from 'vue';
import { useAuthStore } from '../store/index';
import { logout } from '../actions';
// import { AuthStore } from '../constants';

export default defineComponent({
    name: PageName.PROFILE_PAGE,
    setup() {
        const authStore = useAuthStore();
        function changeMessageToStore() {
            useAuthStore.changeMessage();
        }
        const getErrorMessage = authStore.errorMessage;
        const isAuthenticated = authStore.isAuthenticated;
        return {
            getErrorMessage,
            logout,
            isAuthenticated,
            changeMessageToStore,
        };
    },
});
</script>

<style scoped></style>

constants.ts file

import { Store } from 'pinia';

export type PiniaActionAdaptor<
    Type extends Record<string, (...args: any) => any>,
    StoreType extends Store,
> = {
    [Key in keyof Type]: (
        this: StoreType,
        ...p: Parameters<Type[Key]>
    ) => ReturnType<Type[Key]>;
};

export interface AuthStoreState {
    isAuthenticated: boolean;
    errorMessage: null | string;
}

export type AuthStoreActions = {
    changeMessage: () => void;
};

export type AuthStoreGetters = Record<string, unknown>;

export type AuthStore = Store<'auth', AuthStoreState, AuthStoreGetters, AuthStoreActions>;

Folder Store: state.ts file

import { AuthStoreState } from '../constants';

export const authStoreState: AuthStoreState = {
    isAuthenticated: false,
    errorMessage: 'aa',
};

Folder Store: actions.ts file

import { PiniaActionAdaptor, AuthStoreActions, AuthStore } from '../constants';

export const authStoreActions: PiniaActionAdaptor<AuthStoreActions, AuthStore> = {
    changeMessage() {
        this.errorMessage = 'hhh';
    },
};

Folder Store: index.ts file

import { defineStore } from 'pinia';
import { authStoreState } from './state';
import { authStoreActions } from './actions';

export const useAuthStore = defineStore('auth', {
    state: () => authStoreState,
    getters: {},
    actions: {
        ...authStoreActions,
    },
});

i tried creating an actions file outside the store folder, and importing it into SFC vue used to change the state everything seems fine but i don't know how to use actions ?

actions.ts

import { AuthStore } from './constants';

export async function logout(this: AuthStore) {
    this.isAuthenticated = !this.isAuthenticated;
}

Screen Shot 2022-04-16 at 12 20 25 AM

you need this app.use(createPinia())

@tunngtv
Copy link

tunngtv commented Apr 15, 2022

@diadal thanks for your answer, but i want to separate the state, action, getters parts in separate files and will import them into the index file, with Vuex it will be easy to do that, i just learned Pinia and everything is fine, I followed the above instructions by @alexisjamet, but I can't seem to access the action, below is my code using
ProfilePage.vue

<template>
    <div class="profile-page">
        <h1>Profile Page</h1>
        Error Message: {{ getErrorMessage }} <br />
        isAuthenticated: {{ isAuthenticated }}
        <button @click="changeMessageToStore">Change Message</button>
    </div>
</template>

<script lang="ts">
import { PageName } from '@/modules/common/constants';
import { defineComponent } from 'vue';
import { useAuthStore } from '../store/index';
import { logout } from '../actions';
// import { AuthStore } from '../constants';

export default defineComponent({
    name: PageName.PROFILE_PAGE,
    setup() {
        const authStore = useAuthStore();
        function changeMessageToStore() {
            useAuthStore.changeMessage();
        }
        const getErrorMessage = authStore.errorMessage;
        const isAuthenticated = authStore.isAuthenticated;
        return {
            getErrorMessage,
            logout,
            isAuthenticated,
            changeMessageToStore,
        };
    },
});
</script>

<style scoped></style>

constants.ts file

import { Store } from 'pinia';

export type PiniaActionAdaptor<
    Type extends Record<string, (...args: any) => any>,
    StoreType extends Store,
> = {
    [Key in keyof Type]: (
        this: StoreType,
        ...p: Parameters<Type[Key]>
    ) => ReturnType<Type[Key]>;
};

export interface AuthStoreState {
    isAuthenticated: boolean;
    errorMessage: null | string;
}

export type AuthStoreActions = {
    changeMessage: () => void;
};

export type AuthStoreGetters = Record<string, unknown>;

export type AuthStore = Store<'auth', AuthStoreState, AuthStoreGetters, AuthStoreActions>;

Folder Store: state.ts file

import { AuthStoreState } from '../constants';

export const authStoreState: AuthStoreState = {
    isAuthenticated: false,
    errorMessage: 'aa',
};

Folder Store: actions.ts file

import { PiniaActionAdaptor, AuthStoreActions, AuthStore } from '../constants';

export const authStoreActions: PiniaActionAdaptor<AuthStoreActions, AuthStore> = {
    changeMessage() {
        this.errorMessage = 'hhh';
    },
};

Folder Store: index.ts file

import { defineStore } from 'pinia';
import { authStoreState } from './state';
import { authStoreActions } from './actions';

export const useAuthStore = defineStore('auth', {
    state: () => authStoreState,
    getters: {},
    actions: {
        ...authStoreActions,
    },
});

i tried creating an actions file outside the store folder, and importing it into SFC vue used to change the state everything seems fine but i don't know how to use actions ?
actions.ts

import { AuthStore } from './constants';

export async function logout(this: AuthStore) {
    this.isAuthenticated = !this.isAuthenticated;
}

Screen Shot 2022-04-16 at 12 20 25 AM

you need this app.use(createPinia())

yes i know that, and i added it in main.ts

Screen Shot 2022-04-16 at 12 30 06 AM

@tunngtv
Copy link

tunngtv commented Apr 15, 2022

you can separate it simple

Screen Shot 2022-04-15 at 10 11 11 AM

actions.ts

import { axiosInstance, appClient } from '../../boot/axios';
import { Regs, Sget, User, UserKey } from '../../components/models/model';
import { UserDat } from '../../components/models/acclist';
import { RepUser } from '../../components/models/StoreInterFace';

import { useState } from './state';
import { defineStore } from 'pinia';



const REGISTER_ROUTE = '/register';
const LOGIN_ROUTE = '/login';
const AUTH_LOGOUT = '/logout';


const empty = UserDat;

export const useActions = defineStore('auth.actions', () => {
  const state = useState();

  async function register(data: Regs) {
    try {
      const r = await axiosInstance.post(REGISTER_ROUTE, data);
      return r.data;
    } catch (error) {
     
      return false;
    }
  }

  async function login(data: Regs) {
    try {
      const response: {
        data: { status: boolean; data: { user: User; data: string } };
      } = await axiosInstance.post(LOGIN_ROUTE, data);
      const mainData = response?.data?.data;
      const user = mainData?.user;
      if (user) {
        state.user = user;
        const to: string = mainData.data;
        axiosInstance.defaults.headers.common = {
          'content-type': 'application/json',
          'X-Requested-With': 'XMLHttpRequest',
          'M-Version': process.env.APP_VERSION,
          'M-Client': appClient,
          Authorization: `Bearer ${to}`,
        };
        return <RepUser>{ status: true };
      }
      const rep = <RepUser>(
        (response?.data?.data ? response?.data?.data : response?.data)
      );
      return rep;
    } catch (ee) {
      return false;
    }
  }

  function firstlogin(data: { user: User; data: string }) {
    if (data?.user) {
      const newdata = data.user;
      state.user = newdata;
      const to: string = data.data;
      axiosInstance.defaults.headers.common = {
        'content-type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
        Authorization: `Bearer ${to}`,
      };
      return true;
    }
  }

  function updateuser(data: { tok: string; user: User }) {
    const newdata = data.user;
    state.user = newdata;
    return true;
  }

  function userUpdate(data: { id: string; value: boolean | string }) {
    const updateSate = <UserKey>(<unknown>state.user);
    updateSate[data.id] = data.value;
    return true;
  }

  function fetch() {
  
  }

  async function applogout() {
   
  }

  async function logout() {
    
  }
  return {
    register,
    login,
    updateuser,
    userUpdate,
    firstlogin,
    fetch,
    applogout,
    logout,
  };
});

getters.ts

import { defineStore } from 'pinia';
import { computed } from 'vue';
import { User } from '../../components/models/model';
import { useState } from './state';

export const useGetters = defineStore('auth.getters', () => {
  const state = useState();

  const user = computed((): User => {
    return state.user;
  });

  const loggedIn = computed((): boolean => {
    return state.user.login;
  });

  const check = (roles: string | string[]) => {
    const user = state.user;
    if (user && user.roleNames.length > 0) {
      if (Array.isArray(roles) && roles.length) {
        for (const role of roles) {
          if (!user.roleNames.includes(String(role))) {
            return false;
          }
        }
      } else if (roles) {
        const nrole: string = <string>roles;
        if (!user.roleNames.includes(nrole)) {
          return false;
        }
      }
      return true;
    }
    return false;
  };

  return {
    user,
    loggedIn,
    check,
  };
});

index.ts

import { extractStore } from '../extractStore';
import { defineStore } from 'pinia';
import { useActions } from './actions';
import { useGetters } from './getters';
import { useState } from './state';

export const useAuthStore = defineStore('auth', () => {
  return {
    ...extractStore(useState()),
    ...extractStore(useGetters()),
    ...extractStore(useActions()),
  };
});

state.ts

import { defineStore } from 'pinia';
import { UserDat } from '../../components/models/acclist';
import { State } from '../../components/models/StoreInterFace';

export const useState = defineStore({
  id: 'auth.state',
  state: (): State => {
    return {
      user: UserDat,
    };
  },
});

extractStore.ts

import type {
  PiniaCustomStateProperties,
  StoreActions,
  StoreGeneric,
  StoreGetters,
  StoreState,
} from 'pinia';
import type { ToRefs } from 'vue';
import { isReactive, isRef, toRaw, toRef } from 'vue';

type Extracted<SS> = ToRefs<
  StoreState<SS> & StoreGetters<SS> & PiniaCustomStateProperties<StoreState<SS>>
> &
  StoreActions<SS>;

/**
 * Creates an object of references with all the state, getters, actions
 * and plugin-added state properties of the store.
 *
 * @param store - store to extract the refs from
 */
export function extractStore<SS extends StoreGeneric>(
  store: SS
): Extracted<SS> {
  const rawStore = toRaw(store);
  const refs: Record<string, unknown> = {};

  for (const [key, value] of Object.entries(rawStore)) {
    if (isRef(value) || isReactive(value)) {
      refs[key] = toRef(store, key);
    } else if (typeof value === 'function') {
      refs[key] = value;
    }
  }

  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  return refs as Extracted<SS>;
}

StoreInterFace.ts

/* eslint-disable @typescript-eslint/no-explicit-any */
import { Store } from 'pinia';
import { Freg, Regs, User } from './model';

export type PiniaActionAdaptor<
  Type extends Record<string, (...args: any) => any>,
  StoreType extends Store
> = {
  [Key in keyof Type]: (
    this: StoreType,
    ...p: Parameters<Type[Key]>
  ) => ReturnType<Type[Key]>;
};

export type PiniaGetterAdaptor<GettersType, StoreType extends Store> = {
  [Key in keyof GettersType]: (
    this: StoreType,
    state: StoreType['$state']
  ) => GettersType[Key];
};

export type State = {
  user: User;
};

export type Getters = {
  user: User;
  loggedIn: boolean;
  check: (roles: string | string[]) => boolean;
};

export interface RepUser {
  status: boolean;
  data: { user: User; data: string };
}

export type Actions = {
  register?: (data: Regs) => Promise<number | string | Freg> | boolean;
  loggedIn?: () => boolean;
  check?: (roles: string | string[]) => boolean;
  login?: (data: Regs) => Promise<false | RepUser>;
  logout?: () => Promise<boolean>;
  applogout?: () => boolean;
  passwordForgot?: (data: string[]) => Promise<boolean | Freg>;
  passwordReset?: (data: string[]) => Promise<boolean | Freg>;
  updateuser?: (data: { id: string; user: User }) => boolean;
  userUpdate?: (data: { id: string; value: boolean | string }) => boolean;
  firstlogin?: (data: { user: User; data: string }) => boolean;
  newfetch?: () => Promise<boolean | string | User>;
  fetch?: () => boolean;
  user?: () => User;
};

export type AuthStore = Store<'auth', State, Getters, Actions>;

@diadal I tried it this afternoon, but it looks like we are defining more stores, everything is fine, but looking at vue devtools it's weird :D, anyway thank you very much

@diadal
Copy link

diadal commented Apr 15, 2022

you need to clean up. the code to your need also creates your interface based on what you need, if you need to access your store from all your components simply create the plugin helper https://github.com/vuejs/pinia/issues/802#issuecomment-1100196675

app.config.globalProperties.$myStore = helper;

@alexisjamet
Copy link

alexisjamet commented Apr 19, 2022

I refactored it a bit, here is my suggestion:

Folders and Files structure:
Screenshot 2022-04-19 at 8 39 25 AM


Folder /stores

File /types.ts

import { Store } from 'pinia'

export type PiniaActionTree<
  Actions extends Record<string, (...args: any) => any>,
  StoreUsed extends Store,
> = {
  [Key in keyof Actions]: (
    this: StoreUsed,
    ...p: Parameters<Actions[Key]>
  ) => ReturnType<Actions[Key]>
}

export type PiniaGetterTree<
  Getters extends Record<string, (...args: any) => any>,
  StoreUsed extends Store,
> = {
  [Key in keyof Getters]: (
    this: StoreUsed,
    state: StoreUsed['$state'],
  ) => (...p: Parameters<Getters[Key]>) => ReturnType<Getters[Key]>
}

File /index.ts

A single file exporting all my stores (I prefer it this way when importing stores in components):

import { agentStore } from '@/domains/agent/store'
import { AgentStore } from '@/domains/agent/store/types'
import { authStore } from '@/domains/auth/store'
import { AuthStore } from '@/domains/auth/store/types'
import { uiStore } from '@/domains/ui/store'
import { UIStore } from '@/domains/ui/store/types'
import { userStore } from '@/domains/user/store'
import { UserStore } from '@/domains/user/store/types'

export const useAuthStore: () => AuthStore = () => authStore()
export const useAgentStore: () => AgentStore = () => agentStore()
export const useUserStore: () => UserStore = () => userStore()
export const useUIStore: () => UIStore = () => uiStore()

Folder /auth

File /index.ts

import { defineStore } from 'pinia'
import { state } from './state'
import { getters } from './getters'
import { actions } from './actions'

export const authStore = defineStore('auth', {
  state: () => state,
  getters,
  actions,
})

File /types.ts

import { Store } from 'pinia'
import { Actions } from './actions/types'
import { AuthGetters } from './getters/types'

export interface State {
  isAuthenticated: boolean
  errorMessage: null | string
}

export type AuthStore<G = AuthGetters> = Store<'auth', State, G, Actions>

File /state.ts

import { State } from './types'

export const state: State = {
  isAuthenticated: false,
  errorMessage: null,
}

Folder /actions

File /types.ts

import { PiniaActionTree } from '@/stores/types'
import { AuthStore } from '../types'

enum ActionNames {
  CheckReferralCode = 'checkReferralCode',
  Logout = 'logout',
  ResetErrorMessage = 'resetErrorMessage',
}

export type Actions = {
  [ActionNames.CheckReferralCode](code: string): Promise<void>
  [ActionNames.Logout](): Promise<void>
  [ActionNames.ResetErrorMessage](): void
}

export type AuthActions = PiniaActionTree<Actions, AuthStore>

File /index.ts

import { logout } from './logout'
import { resetErrorMessage } from './reset-error-message'

export const actions: AuthActions = {
  checkReferralCode,
  resetErrorMessage,
  logout,
}

File /logout.ts

import router from '@/router'
import { RoutesNames } from '@/router/routes/types'
import Services from '@/services'
import { useUserStore } from '@/stores'
import { AuthActions } from './types'

export const logout: AuthActions['logout'] = async function () {
  await Services.getInstance().authentication.signOut()
  this.isAuthenticated = false
  useUserStore().setUser(null)
  router.push({ name: RoutesNames.Home })
}

Folder /getters

File /types.ts

import { PiniaGetterTree } from '@/stores/types'
import { AuthStore } from '../types'

export enum GetterNames {
  DeepLink = 'deepLink',
}

export type Getters = {
  [GetterNames.DeepLink](): string
}

export type AuthGetters = PiniaGetterTree<Getters, AuthStore<Getters>>

File /index.ts

import { AuthGetters } from './types'
import { deepLink } from './deep-link'

export const getters: AuthGetters = {
  deepLink,
}

File /deep-link.ts

import { API_URL } from '@/external/api/config'
import { AuthGetters } from './types'

export const deepLink: AuthGetters['deepLink'] = function (state) {
  return () => API_URL + '/deeplink/' + state.referralCode
}

Example of using my Pinia stores (actions, state, getters) in components:

<script lang="ts" setup>
import { computed, toRefs, ref, onMounted } from 'vue'
import { useUIStore, useAuthStore } from '@/stores'

const { logout } = useAuthStore() // Store actions
const { isAuthenticated, deepLink } = toRefs(authStore)  // Store state and getters
const { isLoading } = toRefs(useUIStore())  // Store state and getters
</script>

<template>
<h1 v-if="isAuthenticated">You are authenticated</h1>
<div>Here is the result of my getter: {{ deepLink() }}</div>
<button @click="logout">Logout</button>
</template>

@dotlogix
Copy link

dotlogix commented Apr 24, 2022

If someone is interested. I came up with this.
Minimal setup, full type inferance, allows inheritance.

Type Definitions:

piniaTypes.ts:

import { StateTree, Store} from 'pinia'

export type PiniaStateTree = StateTree
export type PiniaGetterTree = Record<string, (...args: any) => any>
export type PiniaActionTree = Record<string, (...args: any) => any>

export type PickState<TStore extends Store> = TStore extends Store<string, infer TState, PiniaGetterTree, PiniaActionTree> ? TState : PiniaStateTree
export type PickActions<TStore extends Store> = TStore extends Store<string, PiniaStateTree, PiniaGetterTree, infer TActions> ? TActions : never
export type PickGetters<TStore extends Store> = TStore extends Store<string, PiniaStateTree, infer TGetters, PiniaActionTree> ? TGetters : never

export type CompatiblePiniaState<TState> = () => TState
export type CompatiblePiniaGetter<TGetter extends (...args: any) => any, TStore extends Store> = (this: TStore, state: PickState<TStore>) => ReturnType<TGetter>
export type CompatiblePiniaGetters<TGetters extends PiniaGetterTree, TStore extends Store> = {
  [Key in keyof TGetters]: CompatiblePiniaGetter<TGetters[Key], CompatibleStore<TStore>>;
}

export type CompatiblePiniaAction<TAction extends (...args: any) => any, TStore extends Store> = (this: TStore, ...args: Parameters<TAction>) => ReturnType<TAction>
export type CompatiblePiniaActions<TActions extends PiniaActionTree, TStore extends Store> = {
  [Key in keyof TActions]: CompatiblePiniaAction<TActions[Key], CompatibleStore<TStore>>;
}

export type CompatibleStore<TStore extends Store> = TStore extends Store<string, infer TState, infer TGetters, infer TActions> ? Store<string, TState, TGetters, TActions> : never

export type PiniaState<TStore extends Store> = CompatiblePiniaState<PickState<TStore>>;
export type PiniaGetters<TStore extends Store> = CompatiblePiniaGetters<PickGetters<TStore>, TStore>;
export type PiniaActions<TStore extends Store> = CompatiblePiniaActions<PickActions<TStore>, TStore>;
export type PiniaStore<TStore extends Store> = {
   state: PiniaState<TStore>,
   getters: PiniaGetters<TStore>,
   actions: PiniaActions<TStore>
}

Counter Store:

actions.ts:

import { PiniaActions, PiniaActionTree } from '@common/piniaTypes';
import { CounterStore } from '@common/counter';

export interface CounterActions extends PiniaActionTree {
  increment(amount: number): void;
}

export const actions: PiniaActions<CounterStore> = {
  increment(amount) {
    this.count += amount;
  },
};

getters.ts

import { PiniaGetters, PiniaGetterTree } from '@common/piniaTypes';
import { CounterStore } from '@common/counter';

export interface CounterGetters extends PiniaGetterTree {
  doubleCount(): number;
  offsetCount(): (offset: number) => number;
  offsetDoubleCount(): (offset: number) => number;
}

export const getters: PiniaGetters<CounterStore> = {
  doubleCount(state) {
    return state.count * 2;
  },
  offsetCount(state) {
    return (amount) => state.count + amount;
  },
  offsetDoubleCount() {
    return (amount) => this.doubleCount + amount;
  },
};

state.ts

import { PiniaState, PiniaStateTree } from '@common/piniaTypes';
import { CounterStore } from '@common/counter';

export interface CounterState extends PiniaStateTree {
    count: number
}

export const state : PiniaState<CounterStore> = () => ({ count: 0 })
import { defineStore, Store, StoreDefinition } from 'pinia'
import { CounterState, state} from '@common/counter/state'
import { CounterActions, actions } from '@common/counter/actions'
import { CounterGetters, getters } from '@common/counter/getters'

export type CounterStore = Store<'counter', CounterState, CounterGetters, CounterActions>
export type CounterStoreDefinition = StoreDefinition<'counter', CounterState, CounterGetters, CounterActions>

export const useStore: CounterStoreDefinition = defineStore('counter', { state, getters, actions })

Inherited Counter Store

extendedCounter.ts

import { defineStore, Store, StoreDefinition } from 'pinia';
import { PiniaStore } from '@common/piniaTypes';
import { CounterState, state } from '@common/counter/state';
import { CounterGetters, getters } from '@common/counter/getters';
import { CounterActions, actions } from '@common/counter/actions';

interface ExtendedCounterState extends CounterState {
  currencySymbol: string;
}

interface ExtendedCounterGetters extends CounterGetters {
  countWithCurrency(): string;
}

type ExtendedCounterStore = Store<'extendedCounterStore', ExtendedCounterState, ExtendedCounterGetters, CounterActions>;
type ExtendedCounterStoreDefinition = StoreDefinition<'extendedCounterStore', ExtendedCounterState, ExtendedCounterGetters, CounterActions>;

const extendedStore: PiniaStore<ExtendedCounterStore> = {
  state: () => ({ ...state(), currencySymbol: '$' }),
  getters: {
    ...getters,
    countWithCurrency(state) {
      return `${state.count} ${state.currencySymbol}`;
    },
  },
  actions: {
    ...actions,
  },
};

export const useExtendedCounter: ExtendedCounterStoreDefinition = defineStore('extendedCounterStore', extendedStore);

Have fun :)

@iamresp
Copy link

iamresp commented Apr 27, 2022

Hi everyone!
Thought a lot on this issue after migrating a large project from Vuex to Pinia and figured out an idea how it can be done. So I ended up with a plugin in attempt to solve it: https://github.com/iamresp/pinia-extract
The whole setup currently looks like this:

import {IPiniaExtractProperties, withExtract} from "pinia-extract";

const pinia = withExtract();

declare module "pinia" {
    export interface PiniaCustomProperties extends IPiniaExtractProperties {}
}

Plugin adds two methods to each store instance: defineAction and defineGetter:

const store = useSomeStore();

export const requestGetCar = store.defineAction(
    async function (id: string) {
        const car = await fetch(`/api/cars/${id}`);
        this.car = await car.json();
    }
);

export const getCar = store.defineGetter(
    (state) => state.car,
);

Externally defined actions can be used as independent functions, for using getters there is a special API:

import {useGetter} from "pinia-extract";
import {getCar} from "./store";

const car = useGetter(getCar);

this in actions points to store instance like in native actions. Getters have state as an argument in a simplest scenario, but, in addition, I made it possible for them to work like selectors in Reselect (it seemed to me that composition it provides fits really nicely here):

export const getCustomerCar = defineGetter(
    getCustomerJobTitle, // first getter
    getCustomerName, // second getter
    getCarModel, // third getter
    getCarType, // fourth getter
    (
        jobTitle: string, // first getter return value
        name: string, // second getter return value
        model: string, // third getter return value
        carType: string, // fourth getter return value
        // ...instead of whole state
    ): string => `${jobTitle} ${name} drives ${model} ${type}`;
);

There are some more features in addition — I tried to describe most of things in readme.
Hope someone will find it helpful.

Thanks.

@power-cut
Copy link

@aparajita I like your solution, does your solution work with mapState & mapActions ?

@aparajita
Copy link

@aparajita I like your solution, does your solution work with mapState & mapActions ?

Haven't tried. I don't see why not, it is easy enough to do a simple test yourself.

@harriskhalil
Copy link

harriskhalil commented May 10, 2022

hi I am newbie dont know too much stuff but i made it work like this
NewUser.ts
`import { defineStore } from 'pinia';
import {state} from 'stores/newuser/state';
import {getters} from 'stores/newuser/getters';
import {actions} from 'stores/newuser/actions';

export const useNewUser = defineStore('NewUser', {

state,
getters,
actions
});
**actions.ts**import axios from 'axios';
import {useNewUser} from 'stores/newuser/NewUser';

export const actions= {

async getU() {
await axios.get('https://www.mecallapi.com/api/users').then((res)=>{
const state = useNewUser()
state.users = res.data

})

},

}
**state.ts**export const state = () => {
return {
users:[]
}
};
**getters.ts**export const getters = {
// user(state){
// return state.users
// }
user: (state :any) => state.users
}
`

@aparajita
Copy link

@harriskhalil We have all tried something that simple, and if it worked believe me we would use it. The first example at the top of this thread is exactly the same as what you propose. If you are using strict type checking it fails, at least with complex state.

@vuejs vuejs locked and limited conversation to collaborators May 27, 2022
@posva posva converted this issue into discussion #1324 May 27, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
contribution welcome 📚 docs Related to documentation changes
Projects
None yet
Development

No branches or pull requests