Vue 3 — The New Store

Vue 3 — The New Store

  • 1305

Vue 3 — The New Store .Is the Vuex Store still necessary in Vue3? The answer for me is definitely NO! And here is why.

Is the Vuex Store still necessary in Vue3? The answer for me is definitely NO! And here is why.

Why You Probably Shouldn’t Use Vuex

If you don’t know Vuex already you can find more information here: https://vuex.vuejs.org/.

Basically, Vuex is a state management plugin for Vue. It provides reactivity out of the box and also integrates nicely into the Vue dev tools.

After using Vuex quite extensively I have to say that I’m not really a fan of it. The reason for that is the unnecessary boilerplate code and structuring of your modules.

What Do Most People Try To Achieve With Vuex?

I think the most common use case is sharing state between your components. Furthermore, they want to ensure that the state of the application can only be changed within specific places of the app.

And in my opinion, it is unnecessary to use Vuex for this use case. You can easily implement your own store without the restriction and complexity of Vuex.

Boilerplate

Let me show you what I mean by that:

let state = {
    count: 0
}

let actions = {
    incrementCount({commit}) {
        commit('incrementCount')
    }
}

let mutations = {
    incrementCount(state) {
        state.count++
    }
}

export default {
    namespaced: true,
    state,
    mutations
}

now let’s look at what we actually need:

export const clickStore = {
    state: {
        count: 0
    },
    incrementCount() {
        clickStore.state.count++;    
    }
}

And with growing stores, the problem only gets bigger since you always have to juggle the mutations and actions. I would suggest using the actions only for mutating the store. Every other kind of logic should be separated into service files.

Since Vuex recommends you only changing the state from within the mutation block you are basically stuck with the boilerplate pattern above. But gladly we will implement an alternative in a few seconds :).

Structure

Sadly you will also deal a lot with strings within your stores. And if you come from a backend language or had some major codebases you know how frustrating it feels refactoring calls that are dependent on strings :).
So committing an action or mutating a store will always lead to firing functions with a String key like this:

incrementCount({commit, dispatch}) {
    commit('incrementCount')
    dispatch('someOtherAction')
}

You already see that refactoring such stuff will be a lot of trouble and you will probably never change the name of a mutation or action since this could potentially mean that your application will break.

The map functions from the Vuex store partially solve this problem as you can see here: https://vuex.vuejs.org/guide/actions.html#dispatching-actions-in-components. But refactoring will still be a problem.

But isn’t refactoring always a problem in JS you might ask?

Yes, that’s right. That is also why I recommend you to use Typescript. I’ve tried to build enterprise software with JS only and it is doomed to fail. I will write a complete blog entry on this topic and link it here later.

And since there isn’t a Vuex typescript implementation that has gained a little bit of traction you are basically lost. The only one I know of is https://github.com/paleo/direct-vuex (which is a really cool project!) and although the author is working actively on the lib I personally wouldn’t use it in an enterprise environment.

So let’s see how we can implement a store with typescript and without the unnecessary boilerplate code :).

Vue 3 to the rescue

So as I’ve already pointed out, you don’t need the Vuex Store with Vue3 anymore. The implementation of your own store is dead simple and way more flexible/maintainable than Vuex stores. Note I will work with typescript in the examples below. If you want to see the matching js files please take a look at https://github.com/Mario-Brendel/Vue3-Tests/tree/store

So first off let’s determine my goal here. I don’t want to have one global state (although this would also be easy to implement) — furthermore, I want to have different stores handling different parts of my application. Ideally, all modules of your application are independent of each other(so are the stores).

Store

I would like to encapsulate the logic regarding Vue reactivity. The developer shouldn’t have to deal with this part. Take a look at how this could be achieved:

import {reactive, readonly} from 'vue';


export abstract class Store<T extends Object> {
    protected state: T;

    constructor() {
        let data = this.data();
        this.setup(data);
        this.state = reactive(data) as T;
    }

    protected abstract data(): T

    protected setup(data: T): void {}

    public getState(): T {
        return readonly(this.state) as T
    }
}

First off I declare a Store class with a generic type parameter which extends an Object. This way I can later ensure that only Objects will be used for the state. You will see why I do this in a couple of seconds.
Next, we look at the constructor which initializes our data and makes it reactive (the part that I want to encapsulate).

This line:

this.state = reactive(data) as T;

is where the magic happens. The reactive function from Vue3 makes our object (provided by the abstract data() method) reactive. Besides that, we also add the as T part. This way we ensure that nobody actually knows that we are dealing with a proxy object and we get the correct code completion :).

In addition, we also have these 3 methods on our abstract class

protected abstract data(): T

protected setup(data: T): void {}

public getState(): T{
    return readonly(this.state) as T
}

For now, you can ignore the first 2 methods but the getState() one is pretty important. Here we are using the readonly function from Vue3. This way we can ensure that the state can ONLY be mutated within the class (this also counts for arrays and nested objects!). So all other instances will only get a read-only version of our object. If you try to mutate the store for example like this:

store.getState().test = “123”

you will get a warning within your developer console and the change will be discarded.

This is why I love Vue3! With Vue2 you never had such power since everything regarded reactivity was tightly coupled to your Vue instances. And if you take a look at above there is no Vue instance — you get all this magic out of the box :).

Store Implementation

Now let’s actually implement the store and use it somewhere. For that, I will create a file called click-store.ts which keeps track of clicks on a given button (basically a store for this little blog entry: https://medium.com/@mario.brendel1990/vue-3-alpha-has-started-d1b3b49869a4).

import {Store} from "./main";

interface Click extends Object {
    count: number
}

class ClickStore extends Store<Click> {
    protected data(): Click {
        return {
            count: 0
        };
    }

    incrementCount() {
        this.state.count++;
    }
}

export const clickStore: ClickStore = new ClickStore()

First off, we provide an interface that describes our state and will be used as the type parameter for our store. This way our data() Method knows that it has to return a Click Object. Within the data() method we now need to initialize the necessary state which then will be made reactive by the Store parent class.

Next, we have our incrementCount() method that doesn’t have to use magically injected functions — like commit in Vuex — to call another function to mutate the state.

…And that’s it. Every new store only has to hook the data() method and provide methods to mutate the store. Let’s take a look at how this might look within a Vue file.

Use Store In A Vue File

<template>
    <div>
        <button @click="inc()">Clicked {{ countState.count }} times.</button>
    </div>
</template>

<script>
    import {clickStore} from "../store/click-store";

    export default {
        setup() {
            const inc = () => {
                clickStore.incrementCount()

                // should throw a warning and don't mutate the store
                clickStore.getState().count++
            }

            return {
                countState: clickStore.getState(),
                inc
            }
        }
    }
</script>

As you can see I’ve created an inc() function which first calls the incrementCount() method provided by the store and afterward tries to change the state directly. The second call will not change anything since we prevent this by using the readonly function.

Lastly, we provide our state via the return. Please keep in mind that you have to provide the state for the template. Otherwise, you will lose the reactivity for your variables, since it is part of the state object and not the individual objects… so if you would write this instead:

count: clickStore.getState().count,

changes wouldn’t be registered within the template.

Conclusion

With only a couple of lines, we are able to provide a nice store solution ourselves which is way easier to maintain and more flexible than most Vuex Stores. Furthermore, with typescript in combination, we get type checks and code completion out of the box which makes life way way easier :).

Although it might seem that I hate Vuex I still think it has its place… but for the majority of projects, it would be better to don’t use Vuex.

All the code above can be found here: https://github.com/Mario-Brendel/Vue3-Tests/tree/store

So if you have any questions regarding this topic or feedback please write a comment or send me an email :).