Sign Up
Hero

State Management with Vuex: a Practical Tutorial

Learn, through a practical tutorial, how to use Vuex to manage the state of your Vue.js apps the right way.

TL;DR: As Single-Page Applications (SPAs) are fast becoming the de-facto way of developing frontend applications, one of the major issues that developers are facing is how they manage state between all the components spread around their applications. Vuejs apps are not an exception to this. To solve this problem, you will learn, through a practical guide, how to use Vuex to manage the state of your Vue.js apps with ease. Check out this article if you need the reference implementation of the app discussed here.

"Looking forward to learn Vuex to manage the state of your @vuejs apps? This is the place."

Tweet This

Prerequisites

Before you begin going through this article, there are two technologies that you should already be familiar with: JavaScript, and Vue.js. You might be able to follow along without having ever used Vue.js before. However, if you don't know JavaScript, you will have a hard time (most likely).

Besides this knowledge, you will need Node.js and NPM installed in your machine. Please, check out this resource to install these tools if you don't have them already (by the way, NPM comes bundled into Node.js).

Vuex in Action

In this section, you learn, through a practical guide, how to use Vuex to build a simple Bitcoin dashboard simulation that shows the current price of Bitcoin and updates it as the time passes. Besides the price, your app will also show the percentage increase and the price difference in real-time. Lastly, your app will show the history of the previous four prices for reference.

To make things organized, you will break your app into four components:

  1. The main component: this one already exists and it is called App.
  2. A component that shows the current Bitcoin price and how it differs from the previous one (this one is the green box in the image below).
  3. A component that shows the percentage change between the current price and the last one, and the time when the update occurred (this one is the blue box in the image below).
  4. A component that shows the Bitcoin pricing history (this one is the table below the blue and the green boxes).

In the end, you will get an app that looks like this:

Note: You won't use real Bitcoin prices here. You will code some fake sample and then generate random values based on this sample.

Scaffolding a new Vue.js App

To scaffold your new Vue.js app with ease, you will use the Vue CLI tool. As such, if you don't have this CLI already installed in your development environment, open a terminal and run the following command:

npm install -g @vue/cli

After a successful installation, navigate to the directory in which you want to keep your project and run the command below to create a new Vue.js app:

vue create bitcoin-dashboard

Running this command will prompt you to select a preset and, if you have Yarn installed, will ask you if you prefer to use it instead of NPM. Feel free to choose the answers that you prefer, but this article will use default for the preset and NPM as the dependency management tool.

After making your choices, the Vue CLI tool will scaffold your app and install the basic dependencies. When done, you can move into your new project and run it to see if everything is working properly:

# move into your new app
cd bitcoin-dashboard

# run it with NPM
npm run serve

Now, if you open http://localhost:8080/ in your browser, you will see the following screen:

Then, to shut down your server, you can hit the following keys: Ctrl + C.

Installing Vuex and Other Dependencies

Alongside with the basic dependencies in your Vue.js app, you will also need to install three other dependencies: Bootstrap, Vuex itself, and Font Awesome (the free version).

To install them, make sure you are on the project root and run the command below:

npm install --save \
  bootstrap vuex @fortawesome/fontawesome-free

Creating the Vuex Store

To begin setting up your Vuex store, you will create a new directory called store inside src. You will use this directory to hold all files related to your Vuex store.

Then, as mentioned before, you will start your Bitcoin app with a fake sample. So, for that, you will need to create a file called prices.js inside the store directory and add the following code to it:

const now = Date.now();
const twoSeconds = 2000;

const prices = [
  {
    amount: 7322.89,
    timestamp: now
  },
  {
    amount: 6322.02,
    timestamp: now - twoSeconds,
  },
  {
    amount: 5222.64,
    timestamp: now - (twoSeconds * 2),
  },
  {
    amount: 5242.61,
    timestamp: now - (twoSeconds * 3),
  }
];

export default prices;

As you can see, in the file that you just created, you are defining and exporting an array of historical Bitcoin prices and the time each one was created. Note that you extract the exact date (Date.now()) that your app starts running to define the time that the last price was created. Also, you create a constant called twoSeconds and use it to calculate the time the other fake prices were created. In other words, you are creating a module that defines an array with four prices:

  • 7322.89: the most recent price that gets the date when the app starts running;
  • 6322.02: the second most recent price that gets the date when the app starts running minus two seconds;
  • 5222.64: the third most recent price that gets the date when the app starts running minus four seconds;
  • and 5242.61: the oldest price that gets the date when the app starts running minus six seconds.

With this file in place, the next thing you will need to do is to create a file where you define the Vuex actions available in your application. To do so, create a file called actions.js inside the store directory and add the following code to it:

const actions = {
  UPDATE_PRICE: 'UPDATE_PRICE'
};

export default actions;

This file only contains a single action because that is the only thing you will need in your application. Having your actions in a separate file this way helps organize your code better and prevents duplicating string names across your app.

Now, you can finally create your store. For that, create a file called index.js inside src and add the following code into it:

import Vue from 'vue';
import Vuex from 'vuex';
import prices from './prices';

// configure Vuex for modules
Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    prices: prices
  },
  getters: {
    currentPrice: state => {
      return state.prices[0];
    },
    previousPrice: state => {
      return state.prices[1];
    },
    percentageIncrease: (state, getters) => {
      const currentAmount = getters.currentPrice.amount;
      const previousAmount = getters.previousPrice.amount;
      return (
        ((currentAmount - previousAmount) / previousAmount) *
        100
      ).toFixed(2);
    },
    difference: (state, getters) => {
      const currentAmount = getters.currentPrice.amount;
      const previousAmount = getters.previousPrice.amount;
      return (currentAmount - previousAmount).toFixed(2);
    }
  },
  mutations: {
    UPDATE_PRICE(state, newPricing) {
      // remove the oldest price
      state.prices.pop();

      // add the new price
      state.prices = [newPricing, ...state.prices];
    }
  }
});

export default store;
export {default as actions} from './actions';

As a lot going on in this file, you will be better off learning it gradually. First, you are importing all modules required:

import Vue from 'vue';
import Vuex from 'vuex';
import prices from './prices';

After that, you are configuring your Vue.js to use Vuex:

Vue.use(Vuex);

Then, you are creating a new instance of a Vuex store (new Vuex.Store) with the configuration object where all your settings are declared. In the configuration object passed to Vuex, the first thing you are defining is the default state object with your fake prices:

state: {
  prices: prices;
}

Then, you are creating a getter to compute and return the current Bitcoin price (currentPrice):

currentPrice: state => {
  return state.prices[0];
},

After that, you are creating another getter for the previous bitcoin price (previousPrice):

previousPrice: state => {
  return state.prices[1];
},

The, you are creating the third getter for the percentage difference between the current price and the previous one (percentageIncrease):

percentageIncrease: (state, getters) => {
  const currentAmount = getters.currentPrice.amount;
  const previousAmount = getters.previousPrice.amount;
  return (
    ((currentAmount - previousAmount) / previousAmount) *
    100
  ).toFixed(2);
},

And, finally, you are creating the last getter for the difference between the current and previous price (difference):

difference: (state, getters) => {
  const currentAmount = getters.currentPrice.amount;
  const previousAmount = getters.previousPrice.amount;
  return (currentAmount - previousAmount).toFixed(2);
}

Next, you are defining the mutations of your Vue.js app. In this application, you only need one mutation which updates the price of Bitcoin:

mutations: {
  UPDATE_PRICE(state, newPricing) {
    // remove the oldest price
    state.prices.pop();

    // add the new price
    state.prices = [newPricing, ...state.prices];
  }
}

As you can see, the UPDATE_PRICE mutation receives the new price data in its payload, removes the oldest price from the prices array, and inserts a new one at the beginning of the array.

Lastly, you are exporting your store and also your actions so other component components can use it:

export default store;
export { default as actions } from './actions';

And that's it! Your Vuex store is fully complete.

Defining your Vue.js Components

Now that your store is all set and ready to go, you will start defining your application components. As mentioned before, your app will contain three components (besides the main one that is called App). One to show the current Bitcoin price and the difference between the previous and current price. One to show the percentage difference between the current and previous Bitcoin prices and that also display when the last update occurred. One component to show the details of a Bitcoin price.

The first one that you will define is the one that will display the current Bitcoin price and the difference between the previous and current price. To define this component, create a file called CoinPrice.vue inside the src/components directory and add the following code to it:

<template>
  <div id="counter" class="bg-success text-white">
    <div class="row p-3">
      <div class="col-2">
        <i class="fas fa-dollar-sign fa-4x"></i>
      </div>
      <div class="col-10 text-right">
        <h2>{{format(price.amount)}}</h2>
        ${{difference}}
      </div>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'CoinPrice',
    computed: {
      price() {
        return this.$store.getters.currentPrice;
      },
      difference() {
        return this.$store.getters.difference;
      }
    },
    methods: {
      format: price => {
        return price.toFixed(2);
      }
    }
  };
</script>

Note: You can also remove the HelloWorld.vue file that Vue.js created by default for you. You won't use the component defined in this file.

As you can see in the <script/> section, the file you just created defines a component called CoinPrice. This component uses two computed properties that retrieve state from the Vuex store: price, which gets the value from $store.getters.currentPrice; and difference, which gets the value from $store.getters.difference. Besides these properties, the file also defines a method called format to return a cleaner price for display in the component template.

Now, in relation to the <template/> area, this file is defining a main <div/> element (with the bg-success and text-white Bootstrap classes to make it look better) with a single child that is a Bootstrap row and then it is splitting the content of this element into two sections:

  • col-2: a section that shows a big (fa-4x) Dollar sign icon (fa-dollar-sign);
  • col-10: a section that will show the current Bitcoin price (amount) and the difference between the current the last price.

After defining this component, you will create a similar one that will show the percentage change between the current and the last price and when this change occurred. To define this one, create a file called PercentChange.vue inside the src/components directory and add the following code to it:

<template>
  <div id="counter" class="bg-primary text-white">
    <div class="row p-3">
      <div class="col-2"><i class="fas fa-percent fa-4x"></i></div>
      <div class="col-10 text-right">
        <h2>
          {{percentageIncrease}}
          <small>%</small>
        </h2>
        {{formatTimestamp(currentPrice.timestamp)}}
      </div>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'PercentChange',
    computed: {
      percentageIncrease() {
        return this.$store.getters.percentageIncrease;
      },
      currentPrice() {
        return this.$store.getters.currentPrice;
      }
    },
    methods: {
      formatTimestamp: timestamp => {
        const currentDate = new Date(timestamp);
        return currentDate.toString().substring(16, 24);
      }
    }
  };
</script>

The component you just defined is quite similar to the previous one. The <template/> structure is basically the same: one big icon (a fa-percent icon in this case) and two values (the percentageIncrease and the timestamp now). Also, the <script/> section is similar, the difference is that now it is showing other relevant data.

Now, the last component you will define is the one that will show some historical Bitcoin price. To define this one, create a file called PriceItem.vue inside the src/components directory and add the following code into it:

<template>
  <li class="list-group-item">
    <div class="row">
      <div class="col-8">${{formatPrice(price.amount)}}</div>
      <div class="col-4 text-right">{{formatTimestamp(price.timestamp)}}</div>
    </div>
  </li>
</template>
<script>
  export default {
    name: 'PriceItem',
    props: {
      price: Object
    },
    methods: {
      formatTimestamp: timestamp => {
        const date = new Date(timestamp);
        return date.toString().substring(16, 24);
      },
      formatPrice: amount => {
        return amount.toFixed(2);
      }
    },
  };
</script>

This component is even simpler than the other two. All this one is doing is defining a <li/> element with the formatted price of some historical value (it can be the current price or an older one) and the time it was created. You will see how to use this component in the next section.

Wrapping Up

You are almost done. Now it is just a matter of opening the App.vue file and replace its code with this:

<template>
  <div id="app" class="container">
    <div class="row">
      <div class="col-12">
        <h1>Dashboard</h1>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-6">
        <CoinPrice/>
      </div>
      <div class="col-sm-6">
        <PercentChange/>
      </div>
    </div>
    <div class="row mt-3">
      <div class="col-sm-12">
        <div class="card">
          <div class="card-header">
            Bitcoin Pricing History
          </div>
          <ul class="list-group list-group-flush">
            <PriceItem v-bind:key="price.timestamp" v-for="price in prices" v-bind:price="price"/>
          </ul>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
  //Components
  import CoinPrice from './components/CoinPrice.vue';
  import PercentChange from './components/PercentChange.vue';
  import PriceItem from './components/PriceItem.vue';
  //Store
  import store, {actions} from './store';
  export default {
    name: 'app',
    components: {
      CoinPrice,
      PercentChange,
      PriceItem
    },
    store,
    computed: {
      prices() {
        return store.state.prices;
      }
    },
    created: function () {
      setInterval(this.triggerNewPrice, 3000);
    },
    methods: {
      triggerNewPrice: () => {
        const diff = (Math.random() - Math.random()) * 10;
        const randomNewPrice = store.getters.currentPrice.amount + diff;
        store.commit(actions.UPDATE_PRICE, {
          amount: randomNewPrice,
          timestamp: Date.now()
        });
      }
    }
  };
</script>
<style>
  @import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
  @import "../node_modules/@fortawesome/fontawesome-free/css/all.min.css";
  #app {
    font-family: "Avenir", Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    color: #2c3e50;
    margin-top: 60px;
  }
</style>

The new version of the App component, although lengthy, is quite simple. The <template/> section starts by defining a title (<h1>Dashboard</h1>). After that, it defines a row where it displays two components: <CoinPrice/> and <PercentChange/>. Lastly, it defines a Bootstrap card (<div class="card">) where it shows the Bitcoin Pricing History. To create this history, the component iterates over each price (v-for="price in prices") to create multiple PriceItem elements.

Then, the <script/> section starts by importing all the components you defined in the previous section alongside with your Vuex store and the actions you created for it. After that, you make your components available to your app by adding them to the components property of your app. You also make your store available by adding it to the App component definition. By importing your store this way, you make it automatically available to all child components of your App component.

After that, three things occur:

  1. You add a computed property called prices to return all the prices available in your Vuex store.
  2. You add a hook into the created event of the lifecycle of your App component to trigger a repetitive task that runs every three seconds. This task starts the creation of new random prices: setInterval(this.triggerNewPrice, 3000);.
  3. You define the triggerNewPrice inside the methods property of your App component to make the function available to it. This function simply generates a random new price and calls the store's commit method with the action to mutate the price (UPDATE_PRICE) issuing with it the new random price as a payload.

Lastly, in the <style/> section, you make your app import the css files of Bootstrap and Font Awesome for use in your application. Oh, and you also add a few custom styles for aesthetics.

Great! With these changes, you are done with your new Vuex app. Time to test it.

To run the application, issue the following command in a terminal (just make sure you are in your project root):

# from the project root
npm run serve

Then, open up your browser and point it to http://localhost:8080/. There, you should see the app running successfully and the prices updating every three seconds. The percentage difference and price difference should also update:

Cool, isn't it!?

"I just learned how to Vuex to manage the state of @vuejs apps. Really cool!"

Tweet This

Aside: Authenticate a Vue App

Using Auth0, you can protect your applications so that only authenticated users can access them. Let's explore how to authenticate a Vue application.

If you would like to have a more in-depth explanation of protecting a Vue application, you can follow this fantastic article: Beginner Vue.js Tutorial with User Login.

Setting up Auth0

To begin, you will need an Auth0 account. You can sign up for a free Auth0 account here. Once you are logged in, follow these steps to set up an Auth0 application.

  1. Go to your Auth0 Dashboard and click the "+ CREATE APPLICATION" button.
  2. Name your new app and select "Single Page Web Applications". Hit "Create".
  3. In the Settings for your new Auth0 application, add http://localhost:8080 to the Allowed Callback URLs, Allowed Logout URLs, Allowed Web Origins. Hit "Save Changes" at the bottom of the page.

Vue application

You will need to install the Auth0 auth0-spa-js SDK. To do so, run the following command:

npm install @auth0/auth0-spa-js

Next, within your src/ folder, create an auth folder. Within the auth folder, create a file named index.js. You should now have a path that is src/auth/index.js.

Within that newly created file, paste in the following code:

// src/auth/index.js

import Vue from 'vue';
import createAuth0Client from '@auth0/auth0-spa-js';

/** Define a default action to perform after authentication */
const DEFAULT_REDIRECT_CALLBACK = () =>
  window.history.replaceState({}, document.title, window.location.pathname);

let instance;

/** Returns the current instance of the SDK */
export const getInstance = () => instance;

/** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */
export const useAuth0 = ({
  onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
  redirectUri = window.location.origin,
  ...options
}) => {
  if (instance) return instance;

  // The 'instance' is simply a Vue object
  instance = new Vue({
    data() {
      return {
        loading: true,
        isAuthenticated: false,
        user: {},
        auth0Client: null,
        popupOpen: false,
        error: null,
      };
    },
    methods: {
      /** Authenticates the user using a popup window */
      async loginWithPopup(o) {
        this.popupOpen = true;

        try {
          await this.auth0Client.loginWithPopup(o);
        } catch (e) {
          // eslint-disable-next-line
          console.error(e);
        } finally {
          this.popupOpen = false;
        }

        this.user = await this.auth0Client.getUser();
        this.isAuthenticated = true;
      },
      /** Handles the callback when logging in using a redirect */
      async handleRedirectCallback() {
        this.loading = true;
        try {
          await this.auth0Client.handleRedirectCallback();
          this.user = await this.auth0Client.getUser();
          this.isAuthenticated = true;
        } catch (e) {
          this.error = e;
        } finally {
          this.loading = false;
        }
      },
      /** Authenticates the user using the redirect method */
      loginWithRedirect(o) {
        return this.auth0Client.loginWithRedirect(o);
      },
      /** Returns all the claims present in the ID token */
      getIdTokenClaims(o) {
        return this.auth0Client.getIdTokenClaims(o);
      },
      /** Returns the access token. If the token is invalid or missing, a new one is retrieved */
      getTokenSilently(o) {
        return this.auth0Client.getTokenSilently(o);
      },
      /** Gets the access token using a popup window */

      getTokenWithPopup(o) {
        return this.auth0Client.getTokenWithPopup(o);
      },
      /** Logs the user out and removes their session on the authorization server */
      logout(o) {
        return this.auth0Client.logout(o);
      },
    },
    /** Use this lifecycle method to instantiate the SDK client */
    async created() {
      // Create a new instance of the SDK client using members of the given options object
      this.auth0Client = await createAuth0Client({
        domain: options.domain,
        client_id: options.clientId,
        audience: options.audience,
        redirect_uri: redirectUri,
      });

      try {
        // If the user is returning to the app after authentication...
        if (
          window.location.search.includes('code=') &&
          window.location.search.includes('state=')
        ) {
          // handle the redirect and retrieve tokens
          const { appState } = await this.auth0Client.handleRedirectCallback();

          // Notify subscribers that the redirect callback has happened, passing the appState
          // (useful for retrieving any pre-authentication state)
          onRedirectCallback(appState);
        }
      } catch (e) {
        this.error = e;
      } finally {
        // Initialize our internal authentication state
        this.isAuthenticated = await this.auth0Client.isAuthenticated();
        this.user = await this.auth0Client.getUser();
        this.loading = false;
      }
    },
  });

  return instance;
};

// Create a simple Vue plugin to expose the wrapper object throughout the application
export const Auth0Plugin = {
  install(Vue, options) {
    Vue.prototype.$auth = useAuth0(options);
  },
};

The comments in this file go over what each section does. To find more details about this file, please visit this blog post section.

Connecting Auth0 and the Vue application

To connect your Auth0 app and your Vue app, you will need to bring over some data from your Auth0 app that you set up earlier. You will want those values protected. To do so, create a file named auth_config.json in the root of your Vue application. Then in the .gitignore, you will want to put that newly created file in there.

In that file, put the following values:

// auth_config.json

{
  "domain": "your-domain.auth0.com",
  "clientId": "yourclientid"
}

Back in your Auth0 dashboard, click on the Settings tab of your Auth0 application. You will find the values "Domain" and "Client ID". Copy and paste those values into this file.

Using authentication globally in Vue

To use this authentication globally within the Vue app, you need to update the src/main.js file. Delete everything in the file and replace with the following code:

// src/main.js

import Vue from 'vue';
import App from './App.vue';
import router from './router';

// Import the Auth0 configuration
import { domain, clientId } from '../auth_config.json';

// Import the plugin here
import { Auth0Plugin } from './auth';

// Install the authentication plugin here
Vue.use(Auth0Plugin, {
  domain,
  clientId,
  onRedirectCallback: (appState) => {
    router.push(
      appState && appState.targetUrl
        ? appState.targetUrl
        : window.location.pathname,
    );
  },
});

Vue.config.productionTip = false;

new Vue({
  router,
  render: (h) => h(App),
}).$mount('#app');

Log in and log out buttons

In order to use all this, you will want to add "Log In" and "Log Out" buttons. To do that, wherever you would like your buttons to be in your application, add this code within the <template> section of that file:

<div v-if="!$auth.loading">
  <!-- show login when not authenticated -->
  <a v-if="!$auth.isAuthenticated" @click="login">Log in</a>
  <!-- show logout when authenticated -->
  <a v-if="$auth.isAuthenticated" @click="logout">Log out</a>
</div>

In that same file within the <script> tag, add in these methods:

<script>
  export default {
    name: 'App',
    methods: {
      // Log the user in
      login() {
        this.$auth.loginWithRedirect();
      },
      // Log the user out
      logout() {
        this.$auth.logout({
          returnTo: window.location.origin,
        });
      },
    },
  };
</script>

You now have the necessary code to authenticate your Vue.js application!

More resources

Conclusion

In this article, you learned, through a practical guide, how easy it is to use Vuex to manage the state of Vue.js apps.

What is cool about Vuex is that, just like Vue.js, it offers a very clean, easy to use, and declarative API for managing state in your applications. Its simplicity does not take anything away from its effectiveness in managing state, regardless of how complex the application is.

What is your opinion about Vuex? Are you planning on using it in your next production-ready app? Besides that, what is your opinion about the article itself? Let us know!