loading...

Vue.js – Chapter 5: State Management

How to Install MySQL 8.0 on Ubuntu 18.04

As an application gets larger and more complex, you start to run into situations where a given piece of state needs to be used in multiple components. State can be information that your app works with—for example, the details of the logged-in user, or the current “state” of some parts of the UI, such as whether a particular piece of functionality is disabled or data is being loaded.

A common solution is to “lift” that piece of state out of the component where it’s being used and into the nearest parent. This can work fine when the components are close siblings, and the state doesn’t reside too far up the tree, but otherwise can lead to what’s called “prop drilling”—having to pass down props through several layers of components that don’t need them themselves.

Fortunately, many popular frameworks have tools that help to manage state. You’ve most likely heard of Redux, a state management system popular in the React community. Vue also has its own official solution, called Vuex. Both of these systems work by having a central store for shared state, and mechanisms for giving access to pieces of that state to any component in your application tree.

In addition, they provide a centralized means to update that state. If different parts of an application can just change the shared state at random, it can lead to hard-to-trace bugs and inconsistencies. Having all changes go via the central store makes the app easier to reason about, and facilitates debugging tools that can log and replay changes to the state.

Installing Vuex

As with the previous chapter, let’s walk through installing Vuex into a fresh project.

Assuming we’re starting with a new CLI-generated project (using the “default” preset), let’s first install the Vuex library from npm:

npm install vuex

Once this has finished downloading, we’ll need to register Vuex with Vue as a plugin and export a new store instance.

Let’s create a new file, store.js, inside the src folder.

src/store.js

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {}
});

We’re passing the Vuex.Store constructor an object with three keys: state, mutations, and actions. Don’t worry about these for now, as we’ll go into each one in the next section.

We need to pass this new store object as an option when we create our main Vue instance, so let’s open up main.js.

src/main.js

import Vue from "vue";
import App from "./App.vue";
import store from "./store";

Vue.config.productionTip = false;

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

As we did with the router in the last chapter, we import our newly created store and pass it to our Vue instance.

By registering the Vuex plugin and passing in a store instance, that store will be made available to every component in our application as this.$store.

Saving time with Vue CLI

As you may already have guessed, if you opt to install Vuex from the Vue CLI, this initial setup will be generated automatically.

Basic Concepts

Now that we have a Vue project with Vuex wired up, let’s take a look at the basic concepts of the library.

State

The store’s state is where all the data you’re managing with Vuex lives. All the other functionality of the library revolves around accessing and updating this data.

The state itself is a single JavaScript object, on which you create properties to hold any data that needs to be shared between different components in your application:

export default new Vuex.Store({
  state: {
    customerName: 'John Smith',
    shoppingCart: [
      {
        name: 'Jumbo Box of Teabags',
        quantity: 1,
        price: 350
      },
      {
        name: 'Packet of Fancy Biscuits',
        quantity: 1,
        price: 199
      },
    ]
  },
});

State properties can contain any valid datatype, from booleans, to arrays, to other objects.

We can access the state in components where we want to use it by creating computed properties:

<template>
  <div>
    <span>{{ customerName }}</span>
  </div>
</template>

<script>
export default {
  computed: {
    customerName() {
      return this.$store.state.customerName;
    }
  }
}
</script>

In this example, the component is accessing the customer name from the store.

Vuex also provides a helper function for accessing the store’s state, to cut down on the boilerplate.

With the mapState helper

<template>
  <div>
    <span>{{ customerName }}</span>
  </div>
</template>

<script>
import { mapState } from "vuex";

export default {
  computed: {
    ...mapState(['customerName'])
  }
}
</script>

The mapState() helper generates computed properties for us from a list of state property names. Notice that we’re using object destructuring: the helpler returns an object of computed properties, so by destructuring these we can easily declare our own properties alongside the helper-generated ones.

Getters

Getters are essentially a Vuex store’s computed properties. They allow you to create derived state that can be shared between different components. Like a component’s computed properties, the output from a getter is cached and only recalculated when the state it depends upon is updated:

export default new Vuex.Store({
  state: {
    shoppingCart: [
      // ...
    ]
  },
  getters: {
    cartItemCount: state => state.shoppingCart.length
  }
});

In the example above, we’ve got a getter called cartItemCount that returns the number of items in the user’s shopping cart.

As when accessing the store state, to use this getter from within a component we need to create a computed property:

<template>
  <div>
    <span>Shopping Cart ({{ cartItemCount }} items)</span>
  </div>
</template>

<script>
export default {
  computed: {
    cartItemCount() {
      return this.$store.getters.cartItemCount;
    }
  }
}
</script>

Vuex provides a mapGetters() helper to eliminate some of the boilerplate:

<template>
  <div>
    <span>Shopping Cart ({{ cartItemCount }} items)</span>
  </div>
</template>

<script>
import { mapGetters } from "vuex";

export default {
  computed: {
    ...mapGetters(['cartItemCount'])
  }
}
</script>

Mutations

One of the main problems with having shared global state in a program is that it can lead to problems that are very hard to debug. Since changes to the state could be coming literally from anywhere in the application, tracking down exactly when and where the state is mutated becomes very difficult.

The Vuex architecture deals with this problem by stating that components never change (or “mutate”) the state directly. Instead, when we want to update the state in some way, we need to use a mutation.

By requiring all state changes to be made via mutations, Vuex can keep track of them. This enables tools like the Vue devtools to present a log of all the mutations applied to the store, allowing you to see exactly what changes were made and when.

If you’ve ever used Redux, a mutation occupies a similar role to a reducer. It takes the existing state and applies changes to it:

export default new Vuex.Store({
  state: {
    customerName: 'Fred'
  },
  mutations: {
    setCustomerName(state, name) {
      state.customerName = name;
    }
  }
});

The code above shows a mutation that changes the customerName state. To use a mutation, it has to be committed to the store using the commit() method of the store:

<template>
  <div>
    <p>{{ customerName }}</p>
    <input type="text" @input="updateName" :value="customerName" />
  </div>
</template>

<script>
import { mapState } from "vuex";

export default {
  name: "Example",
  computed: {
    ...mapState(['customerName'])
  },
  methods: {
    updateName(event) {
      this.$store.commit('setCustomerName', event.target.value);
    }
  }
}
</script>

In the example component above, the customerName state is displayed on the page and in an input control. If the input is changed, the store is updated via the mutation.

Live Demo

You can try this example live on CodeSandbox.

As you might have guessed, there’s a mapMutations helper available:

import { mapState, mapMutations } from 'vuex';

export default {
  name: "Example",
  computed: {
    ...mapState(['customerName'])
  },
  methods: {
    ...mapMutations(['setCustomerName']),
    updateName(event) {
      this.setCustomerName(event.target.value);
    }
  }
}

In this example, we need to extract some data from the event object to send as the mutation’s payload, so we end up having to create an additional method.

You’ll notice that store state is reactive, like instance/component data is. This means that, as soon as a mutation is committed to the store, the changes flow down to your components automatically.

In order for Vuex to effectively keep track of how and when the state is mutated, mutations themselves must be synchronous. So what do we do when we need to run asynchronous code, such as making Ajax requests? That’s where actions come in.

Actions

To perform asynchronous tasks and/or commit multiple related mutations, we need actions. Actions are functions that never change state themselves, but rather delegate to mutations after performing some logic. They receive a context object as their first argument, which contains the keys state, commit, and getters.

As you might guess, state is the store’s state tree, commit is the method for committing mutations to the store, and getters is a collection of all the getters that are defined in the store.

A typical action might do something like fetch some data from a remote API:

import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    users: [],
    isLoading: false,
  },
  mutations: {
    setLoadingTrue(state) {
      state.isLoading = true;
    },
    setLoadingFalse(state) {
      state.isLoading = false;
    },
    setUsers(state, users) {
      state.users = users;
    },
    setCustomerName(state, name) {
      state.customerName = name;
    }
  },
  actions: {
    getUsers(context) {
      context.commit('setLoadingTrue');
      axios.get('/api/users')
        .then(response => {
          context.commit('setUsers', response.data);
          context.commit('setLoadingFalse');
        })
        .catch(error => {
          context.commit('setLoadingFalse');
          // handle error
        });
    }
  }
});

Here, the getUsers action commits a mutation to set the isLoading state to true, and then makes an HTTP request to an API for a list of users. If the request is successful, two mutations are committed to set isLoading back to false, and to add the returned users collection to the store state.

If the request fails, the code toggles isLoading and may do some sort of error handling (for example, committing the error message to state, so it can be displayed somewhere in the app).

Like mutations, actions can’t be called directly. Instead, actions are “dispatched” via a dedicated method on the store, store.dispatch(). Although this can be done from within any component by calling this.$store.dispatch(), it’s usually more convenient to use the mapActions() helper:

<template>
  <div>
    <div  v-if="isLoading">
      <img src="spinner.gif" />
    </div>
    <ul v-else>
      <li v-for="(user, index) in users" :key="index" >{{ user }}</li>
    </ul>
  </div>
</template>

<script>
import { mapActions, mapState } from "vuex";

export default {
  computed: {
    ...mapState([
      'isLoading',
      'users'
    ])
  },
  methods: {
    ...mapActions(['getUsers'])
  },
  created() {
    this.getUsers();
  }
}
</script>

In this example, we’re calling the getUsers action from the component’s created() lifecycle hook. As soon as we do, the isLoading state is updated, and the spinner is displayed. Once the Ajax request has completed, the state is updated again, hiding the spinner and displaying the returned users.

Example

To finish, let’s tie all of these concepts together and add Vuex functionality to our employee directory.

We’ll start off by generating a fresh Vue project with the CLI, and opt to manually select features. In addition to the defaults, we want to check the options for Vue Router and Vuex.

With our fresh project, let’s alter the index page first of all to add some markup and CSS for the demo.

public/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="viewport"
    content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <title>Vuex Example - Jump Start Vue.js</title>
    <link rel="stylesheet" type="text/css"
    href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.css">

    <style type="text/css">
      body {
        background-color: #FFFFFF;
      }
      .ui.menu .item img.logo {
        margin-right: 1.5em;
      }
      .main.container {
        margin-top: 7em;
      }
    </style>
  </head>
  <body>
    <div ></div>
  </body>
</html>

Nothing revolutionary here: just a CDN link to the Semantic UI library, and some additional styling tweaks.

Let’s follow this up by altering the <App> component.

src/App.vue

<template>
  <div>
    <div class="ui fixed inverted menu">
      <div class="ui container">
        <div class="header item">
          <img class="logo" src="./assets/logo.png">
          Jump Start Vue.js
        </div>
        <router-link class="item" to="/" exact>Home</router-link>
        <router-link  to="/users">Users</router-link>
      </div>
    </div>
    <router-view></router-view>
  </div>
</template>

<script>
import { mapActions } from "vuex";

export default {
  name: "App",
  methods: {
    ...mapActions(["fetchUsers"])
  },
  created() {
    this.fetchUsers();
  }
};
</script>

You’ll notice some <router-link> components in the menu, which allow navigating between the pages we’re going to create. The other thing of note in this component is that we’re mapping a fetchUsers action from our store, and we’re calling that as soon as the component is created.

Let’s take a look at the store itself next.

src/store.js

import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    users: [],
    selectedUserId: null,
    isFetching: false
  },
  mutations: {
    setUsers(state, { users }) {
      state.users = users;
    },
    setSelectedUser(state, id) {
      state.selectedUserId = id;
    },
    setIsFetching(state, bool) {
      state.isFetching = bool;
    }
  },
  getters: {
    selectedUser: state =>
      state.users.find(user => user.login.uuid === state.selectedUserId)
  },
  actions: {
    fetchUsers({ commit }) {
      commit("setIsFetching", true);
      return axios
        .get("https://randomuser.me/api/?nat=gb,us,au&results=5&seed=abc")
        .then(res => {
          setTimeout(() => {
            commit("setIsFetching", false);
            commit("setUsers", { users: res.data.results });
          }, 2500);
        })
        .catch(error => {
          commit("setIsFetching", false);
          console.error(error);
        });
    }
  }
});

Hopefully, if you’ve been following along with the chapter so far, the above code should be fairly self explanatory. We’re including the axios library in order to fetch some dummy data from a remote API. There’s also a small timeout before returning the results to simulate network latency and demonstrate the loading state.

Installing axios

In order to build the example, you’ll need to install axios via npm:

npm install axios

Next, we have three page components: Home, Users, and User.

src/views/Home.vue

<template>
  <div class="ui main text container">
    <h1 >Chapter 5: State Management</h1>
    <p>This is a basic Vuex example app, to demo the concepts learned in the
    ➥accompanying chapter.</p>
    <p>Go to <router-link to="/users">Users</router-link></p>
  </div>
</template>

<script>
export default {
  name: "Home"
}
</script>

There’s nothing exciting going on here, but we’re including a link to our /users route that we’ll set up shortly.

src/views/Users.vue

<template>
  <div class="ui main text container">
    <h1 class="ui header">Users</h1>
    <div class="ui active inverted dimmer" v-if="isFetching">
      <div >Loading</div>
    </div>
    <ul v-else>
      <li v-for="(user, index) in users" :key="index">
        <router-link :to="{ name: 'user', params: { id: user.login.uuid }}">
          {{ user.name.title }} {{ user.name.first }} {{ user.name.last }}
        </router-link>
      </li>
    </ul>
  </div>
</template>

<script>
import { mapState } from "vuex";

export default {
  name: "Users",
  computed: {
    ...mapState([
      'isFetching',
      'users'
    ])
  }
}
</script>

<style>
  li {
    text-transform: capitalize;
  }
</style>

In this component, we’re mapping two pieces of state: isFetching, and users. The first is used to display a loading spinner to the user while the users are being fetched from the remote API. The second is the collection of users itself, which we’re iterating over and display as a list of names with links to a dedicated URL for each.

src/views/User.vue

<template>
  <div class="ui main text container" v-if="selectedUser">
    <div class="ui items">
      <div class="item">
        <div class="image">
          <img :src="selectedUser.picture.large">
        </div>
        <div class="content">
          <a class="header">{{ fullName }}</a>
          <div class="meta">
            <span>{{ selectedUser.email }}</span>
          </div>
          <div class="description">
            <p>{{ selectedUser.location.street }}, {{ selectedUser.location.city }},
            {{ selectedUser.location.state }}, {{ selectedUser.location.postcode }}
            </p>
          </div>
          <div >
            {{ selectedUser.phone }}<br />
            {{ selectedUser.cell }}
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { mapGetters, mapMutations } from "vuex";

export default {
  name: "Users",
  computed: {
    ...mapGetters(["selectedUser"]),
    fullName() {
      return `${this.selectedUser.name.first} ${this.selectedUser.name.last}`;
    }
  },
  methods: {
    ...mapMutations(["setSelectedUser"])
  },
  created() {
    const userId = this.$route.params.id;
    this.setSelectedUser(userId);
  }
};
</script>

<style scoped>
  a.header, p {
    text-transform: capitalize;
  }
</style>

This component displays more details about an individual user. It works by getting the user’s ID from the route when the component is created, and committing this to the store via a mutation. This causes the selectedUser getter to update, rendering the component with the chosen user’s details.

Let’s now create some routes for these components.

src/router.js

import Vue from "vue";
import Router from "vue-router";

import Home from "./views/Home.vue";
import Users from "./views/Users.vue";
import User from "./views/User.vue";

Vue.use(Router);

export default new Router({
  mode: "history",
  linkActiveClass: "active",
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    },
    {
      name: "users",
      path: "/users",
      component: Users
    },
    {
      name: "user",
      path: "/users/:id",
      component: User
    }
  ]
});

If you read the previous chapter, there should be nothing to surprise you here. The linkActiveClass option is set, so that the correct class is applied to active links for Semantic UI to style.

Online Demo

You can experiment with this example online at CodeSandbox.

Summary

In this chapter, we looked at what state management solutions are and what problems they help to solve. Let’s recap what we’ve learned:

  • The state is a single JavaScript object holding any data that needs to be shared between components in your application. We can access the state in components by creating computed properties, or by using the mapState helper function.
  • Getters are essentially a Vuex store’s computed properties. They allow you to create derived state that can be shared between different components. To use a getter from within a component, we need to create a computed property, or use the mapGetters()helper.
  • Components never change (or “mutate”) the state directly. When we want to update the state in some way, we need to use a mutation. To use a mutation, it has to be committed to the store using the commit() method. You can also use the mapMutations helper. Mutations must always be synchronous.
  • Actions are functions that never change state themselves, but rather delegate to mutations after performing some logic (often asynchronous). They receive a context object as their first argument, which contains the keys state, commit, and getters. Like mutations, actions aren’t called directly. Instead, actions are dispatched via a dedicated method on the store, store.dispatch(). Although this can be done from within any component by calling this.$store.dispatch(), it’s usually more convenient to use the mapActions() helper.

Comments are closed.

loading...