search

Home  >  Q&A  >  body text

Why is Pinia/Vuex preferred over the classic approach with service classes?

pineapple/cow

Pinia/Vuex as well as Redux are designed to be a "single source of truth", you can have one or more stores to hold application data that can be obtained from anywhere.

Pinia stores look like this:

export let useProductsStore = defineStore('products', () => {
    let data = ref(products);

    function getList (params) {
        return someSearchStuffForProducts(params);
    }

    return {data, getList};
});

can then be used as:

let productsStore = useProductsStore();
console.log(data, data.value);
productsStore.getList(params);

We can create multiple stores:

let usersStore     = useUsersStore();
let productsStore  = useProductsStore();
let basketStore    = useBasketStore();
let favoritesStore = useFavoritesStore();

Stores can reference each other:

export let useUsersStore = defineStore('users', () => {
    let productsStore = useProductsStore();
}

export let useBasketsStore = defineStore('basket', () => {
    let productsStore = useProductsStore();
}

//Et cetera

Finally, Pinia/Vuex are tools that provide the ability to retrieve and manipulate data stored in state.

Manager/Service Class

But there is another mature approach: the manager/service class.

The previous example can be rewritten as:

//Define the "single source of truth"
let store = {
    products:      { /* ... */},
    currentUser:   { /* ... */},
    userBasket:    { /* ... */},
    userFavorites: { /* ... */},
};

//Here goes manager classes
class ProductsManager {
    constructor (params) {
        this.state = params.state;
        //...
    }

    getList (params) {
        return someSearchStuffForProducts(params);
    }
}

class UsersManager {
    constructor (params) {
        this.state = params.state;
        //Products manager is injected as a dependency
        this.productsManager = params.productsManager;
        //...
    }
}

class BasketManager {
    constructor (params) {
        this.state = params.state;
        //Products manager is injected as a dependency
        this.productsManager = params.productsManager;
        //...
    }
}

//Some config/initialization script
export let DIC = {}; //Container for manager instances
DIC.productsManager = new ProductsManager({state: store.products});
DIC.usersManager = new usersManager({
    state:           store.currentUser,
    productsManager: DIC.productsManager,
});
DIC.basketManager = new BasketManager({
    state:           store.userBasket,
    productsManager: DIC.productsManager,
});

//Usage
import {DIC} from './config';
DIC.productsManager.getList();
DIC.basketManager.add(someProductId);
DIC.basketManager.changeCount(someProductId, 3);

All of this can be easily typed in TypeScript without the need for extra wrappers, ref() etc.

discuss

From what I can tell, Pinia looks like "reinventing the wheel": writing the same functionality in a clumsy way.

Additionally, it does not provide dependency injection: you cannot initialize stores in the configuration and inject one store exactly into another, you have to pass useProductsStore() etc.

Inheritance or any other OOP stuff is also not possible.

Pinia even promotes circular dependencies, resulting in spaghetti code with poor maintainability

So, after all, why should one prefer Pinia/Vuex over the tried and tested clean OOP approach with manager classes? I've spent dozens of hours writing a tutorial project of my own invention using Pinia as the "next recommended state management for Vue", and now I'm tempted to rewrite everything into manager classes because I find Pinia clunky and rich . I just remembered that a few years ago I was writing another test project - using Vue2 - and at that time I used the manager class - and everything went smoothly. Did I overlook something? Will there be a problem if I give up Pinia?

P粉358281574P粉358281574329 days ago509

reply all(1)I'll reply

  • P粉781235689

    P粉7812356892024-01-11 16:00:31

    Classes are second-class citizens in Vue reactivity, and there are some pitfalls. They cannot be bound this in the constructor, which will result in using a non-reactive class instance reactive proxy. They cannot effectively use references because these references are documented but in an unusual way . They cannot use get/set accessors to calculate references. These issues require explicitly using the Vue reactive API to write the class in a weird way, or to design the class in a restricted way so reactive(new MyClass) doesn't prevent it from working correctly.

    The

    class does not have the features that a store has, such as extensive support for Vue development tools, plug-in systems, etc.

    Classes are also not serializable in JavaScript, so saving and restoring state requires custom logic rather than simple JSON (de)serialization like in the storage persistence plugin.

    Dependency injection is not unique to classes and can be performed in a suitable way, for example for the Pinia store:

    const basketManagerStore = defineStore({
      state: () => ({ _getFoo: null }),
      getters: { 
        foo: state => state._getFoo()
      },
      actions: {
        setFoo(getFoo) {
          this._getFoo = getFoo;
        }
      }
    });
    
    basketManagerStore.setFoo(useSomeFooStore);

    In many cases it is better to handle Pinia storing composables rather than storing instances, as this resolves circular dependencies that can become a problem if the composable is called prematurely. The same problem can occur with classes and requires the use of a DI container instead of using class instances directly.

    There is no problem with inheritance because reusable code can be handled with FP instead of OOP. Vue doesn't explicitly promote it, but makes the former more idiomatic and comfortable to use.

    TL;DR: Stick with plain objects and FP as this is the main case for Vue's reactive design.

    reply
    0
  • Cancelreply