DEV Community

Cover image for You Might Not Need Vuex with Vue 3
Gábor Soós
Gábor Soós

Posted on

You Might Not Need Vuex with Vue 3

Vuex is an awesome state management library. It's simple and integrates well with Vue. Why would anyone leave Vuex? The reason can be that the upcoming Vue 3 release exposes the underlying reactivity system and introduces new ways of how you can structure your application. The new reactivity system is so powerful that it can be used for centralized state management.

Do You need a shared state?

There are circumstances when data flow between multiple components becomes so hard that you need centralized state management. These circumstances include:

  • Multiple components that use the same data
  • Multiple roots with data access
  • Deep nesting of components

If none of the above cases are true, the answer is easy, whether you need it or not. You don't need it.

But what about if you have one of these cases? The straightforward answer would be to use Vuex. It's a battle-tested solution and does a decent job.

But what if you don't want to add another dependency or find the setup overly complicated? The new Vue 3 version, together with the Composition API can solve these problems with its built-in methods.

The new solution

A shared state must fit two criteria:

  • reactivity: when the state changes, the components using them should update also
  • availability: the state can be accessed in any of the components

Reactivity

Vue 3 exposes its reactivity system through numerous functions. You can create a reactive variable with the reactive function (an alternative would be the ref function).



import { reactive } from 'vue';

export const state = reactive({ counter: 0 });


Enter fullscreen mode Exit fullscreen mode

The object returned from the reactive function is a Proxy object that can track changes on its properties. When used in a component's template, the component re-renders itself whenever the reactive value changes.



<template>
  <div>{{ state.counter }}</div>
  <button type="button" @click="state.counter++">Increment</button>
</template>

<script>
  import { reactive } from 'vue';

  export default {
    setup() {
      const state = reactive({ counter: 0 });
      return { state };
    }
  };
</script>


Enter fullscreen mode Exit fullscreen mode

Availability

The above example is excellent for a single component, but other components can't access the state. To overcome this, you can make any value available inside a Vue 3 application with the provide and inject methods.



import { reactive, provide, inject } from 'vue';

export const stateSymbol = Symbol('state');
export const createState = () => reactive({ counter: 0 });

export const useState = () => inject(stateSymbol);
export const provideState = () => provide(
  stateSymbol, 
  createState()
);


Enter fullscreen mode Exit fullscreen mode

When you pass a Symbol as key and a value to the provide method, that value will be available for any child component through the inject method. The key is using the same Symbol name when providing and retrieving the value.

provide inject

This way, if you provide the value on the uppermost component, it'll be available in all the components. Alternatively, you can also call provide on the main application instance.



import { createApp, reactive } from 'vue';
import App from './App.vue';
import { stateSymbol, createState } from './store';

const app = createApp(App);
app.provide(stateSymbol, createState());
app.mount('#app');


Enter fullscreen mode Exit fullscreen mode


<script>
  import { useState } from './state';

  export default {
    setup() {
      return { state: useState() };
    }
  };
</script>


Enter fullscreen mode Exit fullscreen mode

Making it robust

The above solution works but has a drawback: you don't know who modifies what. The state can be changed directly, and there is no restriction.

You can make your state protected by wrapping it with the readonly function. It covers the passed variable in a Proxy object that prevents any modification (emits a warning when you try it). The mutations can be handled by separate functions that have access to the writable store.



import { reactive, readonly } from 'vue';

export const createStore = () => {
  const state = reactive({ counter: 0 });
  const increment = () => state.counter++;

  return { increment, state: readonly(state) };
}


Enter fullscreen mode Exit fullscreen mode

The outside world will have access only to a readonly state, and only the exported functions can modify the writable state.

By protecting the state from unwanted modifications, the new solution is relatively close to Vuex.

Summary

By using the reactivity system and the dependency injection mechanism of Vue 3, we've gone from a local state to centralized state management that can replace Vuex in smaller applications.

We have a state object that is readonly and is reactive to changes in templates. The state can only be modified through specific methods like actions/mutations in Vuex. You can define additional getters with the computed function.

Vuex has more features like module handling, but sometimes we don't need that.

If You want to have a look at Vue 3 and try out this state management approach, take a look at my Vue 3 playground.

GitHub logo sonicoder86 / vue-3-playground

Vue 3 Playground packed with all the new features

Top comments (23)

Collapse
 
davestewart profile image
Dave Stewart • Edited

Hey,

Nice article!

I've been investigating similar functionality recently, and wrapped up shared stores into a class-based format, that works in Vue 2, 3 and Nuxt and supports state, getters, watches, actions (methods) and inheritance:

github.com/davestewart/vue-class-s...

The library is a single decorator that allows you to use a single interface (classes) and returns you a working store in Vue 2 (as a new Vue) or Vue 3 (using the Reactivity API).

Check the docs for provide/inject example and demos for working code:

github.com/davestewart/vue-class-s...

Collapse
 
sonicoder profile image
Gábor Soós

Nice work, looks amazing 👍

One idea: add an example on how to use it as a plugin...or publish a plugin also in the package

Collapse
 
davestewart profile image
Dave Stewart

Thanks! Can you expand on that?

Thread Thread
 
sonicoder profile image
Gábor Soós

Just an example on the Readme on how to use it with the app instances use method. app.use(store)

Thread Thread
 
davestewart profile image
Dave Stewart

There's no use per-se, you just create the models and use them where you need them.

If you want a "global" style example, either import as needed or use inject exactly as you have (example code in the README):

github.com/davestewart/vue-class-s...

Collapse
 
sylvainpolletvillard profile image
Sylvain Pollet-Villard

What is the benefit of provide/inject compared to directly import { state } ?

Collapse
 
sonicoder profile image
Gábor Soós

The second import solution creates a tightly coupled store, which makes testing, using another store with the same interface, using different stores in different parts of the application hard.

Both solutions work. The second one is simpler but hardly coupled to the implementation. The first one is a bit more complex but makes the code more open to alteration.

Collapse
 
ozzythegiant profile image
Oziel Perez

This is why one must try their best to ensure that with a tightly coupled store, such stores are only used at top level components, leaving the rest to be dumb components used only to display data or emit an event

Collapse
 
dasdaniel profile image
Daniel P 🇨🇦

Vue2 (2.6+) supports similar functionality via Observable.
The downside of switching to alternatives for managing state is the loss of ability to easily debug with the Vue browser extension (the vuex tab specifically). I would caution against using this large scale apps with multiple people working on it, since the conventions around vuex make team and large/long-term dev easier, but for simple apps, I think vuex can easily be replaced with something that may have a simpler learning curve.

Collapse
 
sonicoder profile image
Gábor Soós

Yes, the pro of using existing well-known tools are conventions and that people know it. You can come up with your own conventions and integrate it with Vue devtools also, but yes, it's extra work.

Collapse
 
jcalixte profile image
Julien Calixte • Edited

That's pretty sweet! Simpler than Vuex.

I think I'll use provide and inject for simple states, it's perfect.

But how can we do a cache system like vuex persist?

Collapse
 
sonicoder profile image
Gábor Soós

I would use the watchEffect function for persistence.

Collapse
 
angularnodejs profile image
AngularNodeJS 🚀

It's more simple until you trip up on something and waste your time going down rabbit-holes.

Collapse
 
sonicoder profile image
Gábor Soós

Trip up on what? It's the built-in reactivity system with functions mutating the variables.

Thread Thread
 
angularnodejs profile image
AngularNodeJS 🚀

I was talking about the simple implementation of trying to replace Vuex, it's not a complete solution. Still it is a nice write up on how one would go about using a simple reactive global store.

Thread Thread
 
philippm profile image
Philipp Mildenberger

I have yet to see what real benefits Vuex or in general a Flux-based solution is providing compared to such simple solutions. In my eyes it's more flexible and less boilerplate heavy. If you need more features one can always write up logic around that specifically for the usecase (e.g. what we currently have a model manager that takes care of model instantiation, persistence etc.).
The only really thing that I currently see where vuex has advantages is vue dev tools integration.

Do you have examples where vuex shines compared to that solution?

Thread Thread
 
vitandrguid profile image
Vitor Andrade Guidorizzi

I agree with this, I'm writing a really large vue2 + vuex + ts SPA and thought that vuex would be obligatory, turns out making a really strict typed store with vuex 3 and a lot of store modules is possible but infuriating, its 10 times the boilerplate and whenever you use your store on a component vuex doesn't infer the typing due to its weird string accessor of getters, state and what not.

After fighting against the lib lack of proper TS support i decided to test some libs like vuex-class-modules, vuex-module-decorators and many others, every single one had annoying issues or lacked features.

Looking back, the only thing vuex has that is awesome is the community plugins and the devtools debugger, but i don't think its worth it, with the new composition API i don't see much reason to use it, especially since you can enforce your own accessors and practices instead of the weird soup that is vuex

Collapse
 
brettfishy profile image
Brett Fisher

Great article! I just finished converting a project to use this approach instead of Vuex so that we can use TypeScript. The only downside is you lose the Vuex tools in the Vue devtools extension. Once I upgrade to native Vue 3 instead of the composition API plugin, I might switch back to Vuex just to get the benefits back from the devtools.

Collapse
 
saveroo profile image
Muhammad Surga Savero

Hello, is there any real-world practical example for this ? I'm struggling with non-opinionated approach, so there might be a convenience way of adopting this approach

Collapse
 
sonicoder profile image
Gábor Soós

I haven't done any official project with this approach, but it was completely fine for a TodoMVC app. If you go down this way you can take general convenience ways from vuex and create your own.

Collapse
 
gintsgints profile image
Gints • Edited

Thanks. Wrote my own project using this approach - github.com/gintsgints/quarkus-full...
Just used typescript class and private. May be you can review give suggestions :)

Collapse
 
sonicoder profile image
Gábor Soós

Seems good to me, great work :) . What I would work on is the naming of variables like resp1 to give them more meaning/make them more readable.

Collapse
 
jackydev profile image
JacKyDev

That ist what i search :) Nice article thanks for it