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."

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:

Managing the state of Vue.js apps with Vuex.

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:

Scaffolding a new Vue.js app with the Vue CLI tool.

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:

Managing the state of Vue.js apps with Vuex.

Cool, isn't it!?

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

Aside: Authenticate a Vue App and Node API with Auth0

We can protect our applications and APIs so that only authenticated users can access them. Let's explore how to do this with a Vue application and a Node API using Auth0. You can clone this sample app and API from the vue-auth0-aside repo on GitHub.

Auth0 login screen

Features

The sample Vue application and API has the following features:

  • Vue application generated with Vue CLI and served at http://localhost:4200
  • Authentication with auth0.js using the Auth0 login page
  • Node server protected API route http://localhost:3001/api/meetups/private returns JSON data for authenticated GET requests
  • Vue app fetches data from API once user is authenticated with Auth0
  • Authentication service uses a subject to propagate authentication status events to the entire app.
  • Access token and token expiration are stored in local storage and removed upon logout.

Sign Up for Auth0

You'll need an Auth0 account to manage authentication. You can sign up for a free account here. Next, set up an Auth0 application and API so Auth0 can interface with a Vue.js app and Node API.

Set Up an Auth0 Application

  1. Go to your Auth0 Dashboard and click the "create a new application" button.
  2. Name your new app and select "Single Page Web Applications".
  3. In the Settings for your new Auth0 application, add http://localhost:8080/callback to the Allowed Callback URLs.
  4. Scroll down to the bottom of the Settings section and click "Show Advanced Settings". Choose the OAuth tab and verify that the JsonWebToken Signature Algorithm is set to RS256.
  5. If you'd like, you can set up some social connections. You can then enable them for your app in the Application options under the Connections tab. The example shown in the screenshot above utilizes username/password database, Facebook, Google, and Twitter. For production, make sure you set up your own social keys and do not leave social connections set to use Auth0 dev keys.

Set Up an API

  1. Go to APIs in your Auth0 dashboard and click on the "Create API" button. Enter a name for the API. Set the Identifier to a URL. In this example, this is http://meetupapi.com/. The Signing Algorithm should be RS256.
  2. You can consult the Node.js example under the Quick Start tab in your new API's settings. We'll implement our Node API in this fashion, using Express, express-jwt, and jwks-rsa.

We're now ready to implement Auth0 authentication on both our Vue client and Node backend API.

Dependencies and Setup

The Vue app utilizes the Vue.js CLI. Make sure you have the CLI installed globally:

$ npm install -g vue-cli

Once you've cloned the project, install the Node dependencies for both the Vue app and the Node server by running the following commands in the root of your project folder:

$ npm install
$ cd server
$ npm install

The Node API is located in the /server folder at the root of our sample application.

Find the config.js.example file and remove the .example extension from the filename. Then open the file:

// server/config.js
// (formerly config.js.example)
module.exports = {
  CLIENT_DOMAIN: '[CLIENT_DOMAIN]', // e.g. 'you.auth0.com'
  AUTH0_AUDIENCE: 'http://meetupapi.com'
};

Change the CLIENT_DOMAIN variable to your Auth0 application domain and set the AUTH0_AUDIENCE to your audience (in this example, this is http://meetupapi.com). The /api/examples/private route will be protected with express-jwt and jwks-rsa.

Note: To learn more about RS256 and JSON Web Key Set, read Navigating RS256 and JWKS.

Our app and API are now set up. They can be served by running npm run dev from the root folder and node server.js from the /server folder.

With the Node API and the Vue.js app running, let's take a look at how authentication is implemented.

Authentication Service

Authentication logic on the front end is handled with an Auth authentication service: src/auth/Auth.js file.

// src/auth/Auth.js
/* eslint-disable */
import auth0 from 'auth0-js';
import router from '../router';

export default class Auth {

  auth0 = new auth0.WebAuth({
    domain: AUTH0_DOMAIN, // e.g., you.auth0.com
    clientID: AUTH0_CLIENT_ID, // e.g., i473732832832cfgajHYEUqiqwq
    redirectUri: CALLBACK_URL, // e.g., http://localhost:8080/callback
    audience: AUTH0_API_AUDIENCE, // e.g., https://meetupapi.com
    responseType: 'token',
    scope: 'openid'
  });

  constructor() {
    this.login = this.login.bind(this);
    this.handleAuthentication = this.handleAuthentication.bind(this);
    this.logout = this.logout.bind(this);
  }

  handleAuthentication() {
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken) {
        this.setSession(authResult);
        router.replace('/');
      } else if (err) {
        router.replace('/');
      }
    })
  }

  setSession(authResult) {
    // Set the time that the access token will expire at
    const expiresAt = JSON.stringify(authResult.expiresIn * 1000 + new Date().getTime());
    localStorage.setItem('access_token', authResult.accessToken);
    localStorage.setItem('expires_at', expiresAt);
  }

  requireAuth(to, from, next) {
    if (! (new Auth).isAuthenticated()) {
      next({
        path: '/',
        query: { redirect: to.fullPath }
      });
    } else {
      next();
    }
  } 


  login() {
    this.auth0.authorize();
  }

  logout() {
    // Clear access token and expiration from local storage
    localStorage.removeItem('access_token');
    localStorage.removeItem('expires_at');
    // navigate to the landing page route
    router.go('/');
  }

  isAuthenticated() {
    // Check whether the current time is past the
    // access token's expiry time
    const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
    return new Date().getTime() < expiresAt;
  }
}

Replace the constants, AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_API_AUDIENCE with values from your Auth0 dashboard. Replace CALLBACK_URL with http://localhost:8080/callback.

The login() method authorizes the authentication request with Auth0. An Auth0 login page will be shown to the user and they can then log in.

Note: If it's the user's first visit to our app and our callback is on localhost, they'll also be presented with a consent screen where they can grant access to our API. A first party application on a non-localhost domain would be highly trusted, so the consent dialog would not be presented in this case. You can modify this by editing your Auth0 Dashboard API Settings. Look for the "Allow Skipping User Consent" toggle.

We'll receive accessToken and expiresIn in the hash from Auth0 when returning to our app. The handleAuthentication() method uses Auth0's parseHash() method callback to set the session (setSession()) by saving the tokens, and token expiration to local storage. The isAuthenticated method informs the components in the app about the user's authentication status via checking the access token's expiry time.

Finally, we have a logout() method that clears data from local storage.

Callback Component

The callback component is where the app is redirected after authentication. This component simply shows a loading message until the login process is completed. It executes the handleAuthentication() method to parse the hash and extract authentication information.

// src/components/Callback.vue
<template>
  <div>
      <h3>Loading....</h3>
  </div>
</template>
<script>

import Auth from '../auth/Auth.js';

const auth = new Auth();

export default {
  name: '',
  mounted() {
    this.$nextTick(() => {
      auth.handleAuthentication();
    });
  },
};
</script>

Making Authenticated API Requests

In order to make authenticated HTTP requests, we need to add an Authorization header with the access token in our meetup-api.js file.

// utils/meetup-api.js
/* eslint-disable */
import axios from 'axios';
import Auth from '../src/auth/Auth.js';

const auth = new Auth();
const BASE_URL = 'http://localhost:3333';

export function getPublicMeetups() {
  const url = `${BASE_URL}/api/meetups/public`;
  return axios.get(url).then(response => response.data).catch(err =>  err || 'Unable to retrieve data');
}

export function getPrivateMeetups() {
  const url = `${BASE_URL}/api/meetups/private`;
  return axios.get(url, { headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` }}).then(response => response.data).catch(err => err || 'Unable to retrieve data');
}

Final Touches: Route Guard and Private Meetups Page

A Private meetup page component can show information about private meetups. However, we only want this component to be accessible if the user is logged in.

The route guard is implemented on specific routes of our choosing in the router/index.js file like so:

// src/router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import PublicMeetups from '@/components/PublicMeetups';
import PrivateMeetups from '@/components/PrivateMeetups';
import Callback from '@/components/Callback';
import Auth from '../auth/Auth';

const auth = new Auth();

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'PublicMeetups',
      component: PublicMeetups,
    },
    {
      path: '/private-meetups',
      name: 'PrivateMeetups',
      beforeEnter: auth.requireAuth,
      component: PrivateMeetups,
    },
    {
      path: '/callback',
      component: Callback,
    },
  ],
});

More Resources

That's it! We have an authenticated Node API and Vue.js application with login, logout, and protected routes. To learn more, check out the following 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!