loading...

Vuex: Why We Need SPA State Management

malgamves profile image Daniel Madalitso Phiri Originally published at blog.logrocket.com ・8 min read

One of the most beautiful things about Vue.js is the relative simplicity it brings to modern web development, building Single Page Applications has never been easier. JavaScript frameworks like Vue came with component based design patterns. Whole web applications are just a collection of individual pieces (components) sharing data, the bigger the application gets, the harder it is for data to remain consistent and be managed in each individual component. This data is commonly referred to as application state. For Vue.js, Vuex is the most widely used state management library, today we’ll go into adding and integrating Vuex into a Vue.js applications.

Not only does Vuex work as a central store for your application state, it sets rules to ensure data is changed in a way that is expected. Vuex ensures your views remain consistent with your application data. Don't worry if this doesn't make sense now, it'll all come together as we go on and build something.

As an semi-regular conference and event goer, I tend to meet a bunch of people, during our interactions I agree to do certain stuff that I most certainly always forget. So we’re going to build something literally no one else but me will use - a reminder (glorified to-do list) app.

Before we dive into it, here’s a few things you’ll need:

  • Basic knowledge of Vue.js
  • Node.js and Yarn installed

Alright!! We’ve already covered what Vuex does and why it does it. We need to setup our project, open your terminal and type vue create <project-name> to do so, you’d need the Vue CLI installed. If you don’t have that installed you can get it here. Select the default project setup and once everything is done and we have our project initialized, run cd <project-name> and yarn serve. You should see your usual vue starter page

After getting this running, we need to add vuex to our project. In your terminal, type vue add vuex and after, you should see your directory structure change quite a bit. As with most state management tools, Vuex has a central store/single state tree to store application state, ours is in the src folder, you get a store.js file or a store folder with an index.js file. If not you can create them and paste the following code in

    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex)
    export default new Vuex.Store({
      state: {
      },
      mutations: {
      },
      actions: {
      }
    })

You will also see a change in src/main.js, as we import the store. If not, paste the following code,

    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')

At this point looking at store, you’re probably wondering what all the sections are for. We’ll briefly go over them before we can go deeper.

State: Application state is the data your application uses.

Mutations: Synchronous method of changing store state and directly commit to change state.

Actions: Commit mutations and give way for for asynchronous operations.

bonus

Getters: Computed properties derived from store state.

Don’t worry if it doesn’t all make sense now, we’ll get into building to make it easier. We’ve just added Vuex to our project, now we have to test it. We’ll start out by defining some data for our store. In your store, we’ll define a new data property called username by pasting username: "danielphiri" into the state portion of our store. We want to make this show on our webpage, in HelloWorld.vue, clear the tag and paste the following

    <template>
      <div>
        <h1> {{ username }} </h1>
      </div>
    </template>

In the <script> section of the same file, we need to add import mapState from 'vuex' and paste the following

    computed: {
        ...mapState(["username"])
      }

We should then see the value we kept in our store displayed on the screen.

Now getting into the core of the reminder app we want to build, we will need to be able to input task details and who we need to perform them for. We should also be able to dismiss all tasks or individual tasks. We need to conceptualize a data model for the state so we know what data we’re using up in the HTML portion of our application. In your store, paste the following code

    state: {
        username: "danielphiri",
        tasks: [
          { taskName: "take pictures", taskReciever: "mom and dad" },
          { taskName: "email organisers slides", taskReciever: "myself" },
          { taskName: "send resume", taskReciever: "dev job" },
        ]
      },
      mutations: {
        ADD_TASK: (state, task) => {

          state.tasks.push(task);
        },
        REMOVE_TASK: (state, task) => {
          state.tasks.splice(task, 1);
        },
      actions: {
        removeTask: (context, task) => {
          context.commit("REMOVE_TASK", task);
        },
      }

In our state, we define a username and an array that holds our tasks and related data. We define two mutations, ADD_TASK which changes the state by adding a task to the tasks array, and REMOVE_TASK that removes a task from the tasks array. Lastly we define an action removeTask that gives the option a remove tasks asynchronously with some custom logic. You will notice the context object as the first argument in removeTask, actions in vuex use context which gives them access to store properties and methods like context.commit() which it used to commit a mutation.

To get started we’ll create a component to allow us to input tasks and display them as well as remove them. Let’s call this Main.vue and we’ll paste the following code in the <script> section:

Don’t forget to add your Main component to your App.vue file.

    <script>
    import { mapState, mapMutations, mapActions } from "vuex";
    export default {
      name: "Main",
      data() {
        return {
          taskName: "",
          taskReciever: "",
        };
      },
      computed: {
        ...mapState(["tasks", "username"])
      },
      methods: {
        ...mapMutations(["ADD_TASK"]),
        ...mapActions(["removeTask"]),
        addTask: function() {
          let newTask = Object.create(null);
          newTask["taskName"] = this.taskName;
          newTask["taskReciever"] = this.taskReciever;
          this.ADD_TASK(newTask);
          this.taskReciever = "";
          this.taskName = "";
        },
        removeTasks: function(task) {
          this.removeTask(task);
        }
      }
    };
    </script>

At the top of the file, you will notice that we import a couple of helper functions. They’re all pretty similar in functionality, mapState for example helps us map store state to local (component) computed properties. So mapMutations does the same for store mutations and mapActions for store Actions. We go on to use mapState to enable us display “username" and “tasks" in our component. We also use mapMutations in the methods property so we can call store mutations as functions with parameters as we did when we defined addTask() which we use to perform mutations while passing the newTask object as a parameter.

In the section of our Main.vue, we paste the following code

    <template>
      <div class="home">
        <div class="hello center">
          <div >
            <h1 class="header-text"> Hi 👋, {{ username }}</h1>
            <h3 class="header-text"> Add a few tasks</h3>
            <form @submit.prevent="addTask">
              <input class="input" type="text" placeholder="I'm supposed to.." v-model="taskName" />
              <input class="input" type="text" placeholder="for this person..." v-model="taskReciever" />
              <button class="add-button" type="submit" placeholder="Add task to list">Add task to list</button>
            </form>
            <ul>
              <li v-for="(task, index) in tasks" v-bind:key="index">
                {{ task.taskName }} for {{task.taskReciever}}
                <button
                  v-on:click="removeTasks(index)"class="remove">Done ✅</button>
              </li>
            </ul>
          </div>
          <div class></div>
        </div>
      </div>
    </template>

We can directly interpolate our username from store because we mapped it as a computed property using mapState, the same goes for the tasks. We use v-for to loop over the tasks array from our store and display all our tasks their properties i.e taskName and taskReciever . We also use a form to mutate tasks to our store. On submit (@submit) a.k.a when we press the button after filling in tasks, we call the addTask method which then changes our state by adding whatever we input to the tasks array. Optionally you can add a style section by pasting this

    <style>
    html,
    #app,
    .home {
      height: 100%;
    }
    body {
      background-color: #050505;
      margin: 0;
      height: 100%;
    }
    input {
      border: none;
      padding: 5%;
      width: calc(100% - 40px);
      box-shadow: 0 3px 3px lightgrey;
      margin-bottom: 5%;
      outline: none;
    }
    .header-text {
      color: #e9e9e9;
    }
    .add-button {
      border: none;
      border-radius: 2px;
      padding: 5%;
      background-color: #0cf50cbb;
      box-shadow: 0 2px 2px #fff;
      width: calc(100% - 100px);
      margin-bottom: 2%;
      outline: none;
    }
    .main {
      display: grid;
      grid-template-columns: repeat(2, 50%);
      grid-template-rows: 100%;
      height: 100%;
    }
    .center {
      display: flex;
      justify-content: center;
    }
    .left,
    .right {
      padding: 30px;
    }
    ul {
      list-style-type: none;
      padding: 0;
    }
    ul li {
      padding: 4%;
      background: white;
      margin-bottom: 8px;
      border-radius: 5px;
    }
    .right {
      grid-area: right;
      background-color: #e9e9e9;
    }
    .remove {
      float: right;
      text-transform: uppercase;
      font-size: 0.8em;
      background: #050505;
      border: none;
        border-radius: 5px;
      padding: 5px;
      color: #00ff88de;
      cursor: pointer;
    }
    </style>

Save your work and run it you should see this.

Right now, we have some basic vuex operations working but you can’t really tell why we use vuex, we’re only using a single component. Let’s create another compenent called Stats.vue, we’ll use this to display a few stats and show how vuex actions can be properly put to use.

For starters, we want to be able to display the number of pending tasks we have, in our store we can define a getter to do this by pasting the following text below the state object,

    getters: {
        taskCount: state => {
          return state.tasks.length;
        }
      },

We then add another mutation to the store

    REMOVE_ALL: state => {
          state.tasks = [];
        },

This lets us clear every task in our list. Finally in our state, we add another action to the store right below removeTask by adding the following code.

    removeAll({ commit }) {
          return new Promise((resolve) => {
            setTimeout(() => {
              commit("REMOVE_ALL");
              resolve();
            }, 2000);
          });
        }

You notice we define a promise and use setTimeout function to add a bit of a delay (2 seconds) before we commit our REMOVE_ALL mutation. Thus the asynchronous nature of vuex actions. We’re free to play around with the logic that dictates how we perform actions, this could be used in a shopping cart, trading website, chat application - it has so many uses.

Back to our Stats.vue file, we paste the following in the <script> section

    <script>
    import { mapGetters, mapActions, mapMutations, mapState } from 'vuex'
    export default {
      name: 'Stats',
      computed: {
        ...mapGetters(['taskCount']),
        ...mapState(["username"])
      },
      data() {
        return {
          message: ""
        }
      },
      methods: {
        ...mapMutations(['REMOVE_ALL']),
        ...mapActions(['removeAll']),
        removeAllTasks() {
          this.removeAll().then(() => {
            this.message = 'Self care - tasks are gone'
          });
        }
      }
    }
    </script>

In Stats.vue, like we said, we wanted to be able to count how many tasks we have pending. We use the mapGetters helper to display that computed property. In methods, we initialize our removeAll action and REMOVE_ALL mutations as well as define removeAllTasks which remember, has a promise and lets us use the then() prototype to display text once the promise is fulfilled.

In the <template> section of Stats.vue, paste the following code

    <template>
      <div class="stats">
        <h3 class="header-text">Here are your numbers, {{username}} 😬 </h3>
        <p class="header-text">You need to perform {{ taskCount }} tasks fam</p>
        <button class="" v-on:click="removeAllTasks">Nope, can't even..</button>
        <p class="header-text">{{ message }}</p>
      </div>
    </template>

Here we have a button to remove all the tasks and a message that gets displayed when our promise is fulfilled.

Run your app and you should have a pretty nifty web app like this

Conclusion

We went over why we need Vuex, Vuex operations and helpers and built an app using it. We have a functional web app that you can test out here, we say how we can use Vuex to manipulate a single data source and avoid inconsistencies. We built a multi component app and shared data between them,

Should you want to dive deeper in the topic I recommend the following resources.

Check out the full version this on GitHub or CodeSandbox. I hope you enjoyed this and if you have any questions or want to say hi, feel free to tweet at me. Till next time.

Posted on by:

malgamves profile

Daniel Madalitso Phiri

@malgamves

Tech x Culture - Tech Writer - Disk Jockey - Host #RushingFwd - @devcon_zm, @CodeCastZM - Prev. @HasuraHQ - Int'l Speaker - Zambian 🇿🇲 #DevRel

Discussion

pic
Editor guide