Inheritance / shared action and getters in Pinia
Asked Answered
J

3

7

I have a couple of Pinia stores that should share a set of actions and getters and I’m not quite sure how to effectively achieve that.

I’m building an app that lets users manage a number of different media (Books, Movies, TV Shows, etc). The way I’m currently thinking about it is to have a store for each media type e.g. BookStore, MovieStore etc. A lot of the getters and actions (e.g., count and deleteOne) are exactly the same between those different stores.

How do I achieve DRY here? The Pinia documentation has examples that mostly focus around reusing actions and getters inside other stores but I don’t think that quite solves my use case of inheriting a set of getters and setters outright.

Is my attempted inheritance approach here an anti-pattern?

Jojo answered 5/11, 2022 at 20:10 Comment(1)
Consider reasking with more details on your case. The way this can be done depends on the actual requirements. Notice that extracting actions, etc from store definition can be bad because you lose the inference of this inside them in TS.Concentration
C
7

This is achievable using plugins docs

Example Movies:

You have multiple stores using shared naming scheme for each state:

  • item: single entity item (single movie details)
  • collection: collection of items (collection of all movies)

each store will have the same CRUD actions with only the URL changing

  • getCollection: get list of items from API and set response as collection (https://url.com/movies)
  • getItem: get single item from API and set response as item (https://url.com/movies/id)
  • handleError: displays alert to the user with error information

Create plugin:

function BaseStorePlugin () {
    return {
        collection: [],
        item: {},
        getCollection: function (url) {
            api.get(url)
                .then((response) => {
                    this.collection = response.data;
                })
                .catch((error) => {
                    this.handleError(error);
                });
        },
        getItem: function (url) {
            api.get(url)
                .then((response) => {
                    this.item = response.data;
                })
                .catch((error) => {
                    this.handleError(error);
                });
        },
        handleError: function (error) {
            window.alert(error);
        },
    };
}

Give plugin to Pinia:

const pinia = createPinia();

pinia.use(BaseStorePlugin);

Example movieStore.js (using shared action & state)

import { defineStore } from 'pinia';
import { api } from 'src/boot/axios';

export const useMovieStore = defineStore({
    id: 'movie',
    state: () => ({
        movieSpecificStateObject: {},
    }),
    actions: {
        movieSpecificAction (url) {
            console.log(this.item);
            api.get(url)
                .then((response) => {
                    // handle response
                })
                .catch((error) => {
                    this.handleError(error);
                });
        },
    },
});

Example usage in component

<template>
    <div
        v-for="movie in movieStore.collection"
        :key="movie.id"
    >
        <div>
            {{ movie.name }}
        </div>
    </div>
</template>

<script setup>
import { onMounted } from 'vue';
import { useMovieStore } from 'src/stores/movieStore.js';
const movieStore = useMovieStore();

onMounted(() => {
    movieStore.readCollection('http://url.com/movies');
});
</script>

Edit: 1

if you pass the context into the plugin you have access to the store and options being passed into it, from this you could check the store id and only return for specific stores like below

function BaseStorePlugin (context) {
  const allowedStores = ['movie', 'album'];

  if (allowedStores.includes(context.store.$id)) {
    return {
      collection: [],
      getCollection: function () {
        const fakeCollection = Array.from({length: 10}, () => Math.floor(Math.random() * 40));
        fakeCollection.forEach((item) => {
          this.collection.push({
            id: item,
            name: `name${item}`
          });
        });
      },
    };
  };
}

I have created a very basic example using 3 stores and the above check available on codesandbox here

Cheremkhovo answered 6/11, 2022 at 13:25 Comment(4)
Thank you for your thoughtful answer @Multi! I'll give this a try tonight. Is there a way to apply the plugin only to some Store instance? The documentation seems to allude to that ability but I can't figure out how it's actually done.Jojo
have updated the answer with more information and added linkCheremkhovo
I can't thank you enough @Multi. One more silly question, is it possible to inherit getters (in addition to the actions we're inheriting here)Jojo
it is possible just by creating a function (action) that returns a value but I'm not sure if this is the actual correct way to do this. to have it have it reactive you may need to create the function as a computed property and return thatCheremkhovo
E
2

If you'd like to keep some functions shared between not all stores, you can use composables way.

You can make a separate composable function and pass a part of store instance into it.

I made an example on codesandbox for you.

Short example of codesandbox here:

common.ts

import { computed, Ref, ref } from "vue";

export function useCommon(initValue: number) {
    const _value = ref<number>(initValue);

    function increment() {
        _value.value++;
    }

    function someProcessing() {
        // ... some code here
    }

    return {
        counter,

        increment,
        someProcessing,
    };
}

Then in any store you can use it like:

fooStore.ts

export const useFooStore = defineStore('foo', () => {
    const state = ref<string>('foo');

    const { counter, increment, someProcessing } = useCounter(0);

    return {
        state,

        counter,
        increment,
        someProcessing,
    }
}

By this way you can compose any set of functions, object and etc. in any store or in any component.

Eneidaenema answered 25/2, 2023 at 9:48 Comment(1)
How do I have private state variables within the composable function?Marbleize
M
0

@Multi's sample is great and working perfectly.I just want to point one spot and improve his code a bit. when you call common action the data you fetched is stored in common state not in specific store state and you need to point your getters to main store to get specific data, but thanks to @multi again if you use context function you can store the data to the specific store state.like below

 .then((response) => {
      console.log("this store", context.store);
      this.collections[payload.table] = response.data.records;// save data  in common store state 
      context.store.<specific state variable>= response.data.records;// save data to specific store state

      this.apiState = ENUM.LOADED;
    })

than you can call data with

import { useMovieStore } from "src/stores/movies.js"; 
const movieStore = useMovieStore ();
console.log(movieStore.<specific state variable>)
Mascagni answered 21/4, 2023 at 0:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.