What a start to the year! Let me set the stage: I’m in the process of building a cash register / point of sale system, which uses Vue.js and Vuex.

The purpose of the app is to enter food orders and print them to a receipt (a story for another time!)

However, when I deployed a physical unit, one immediate piece of feedback from hungry customers is that they’d like to see the order as the food items are being entered. Woops! Didn’t think of that.

As luck might have it, the point of sale unit I was working on had a second display on the back side. “In that case,” you might ask, “why not just mirror the displays to show the contents?” And you know what? That’s totally fair. Sure, it’s not pretty to also show the available items on the second display, but for a fast turnover (given that I’d already deployed the unit) that’ll do just fine.

Nope, the issue turned out to be that the second display is considerably smaller than the main one, forcing the first screen into an 800 * 600 resolution, which frankly won’t do at all.

The client who I was installing the point of sale system for then suggested mirroring only a portion of the screen. I’d never done it before it must be possible, right?

In short, it is, but with some wonky, proprietary third-party solutions that frankly created more problems than they solved.

Ok so where do Vue and Vuex come in?

Right! Well, after kicking some other ideas around, I suggested having a separate browser window on the second screen and somehow synchronize the two.

Maybe we can somehow keep the Vuex store in sync between tabs?

How the Vuex store was set up leading up to this brainwave

Here’s how it roughly looked at the time:

const OrderItemStore = {
  namespaced: true,
  state: {
    orderItems: [],
  },
  mutations: {
    reset(state) {
      state.orderItems = []
      return state;
    },
    add(state, item) {
      state.orderItems.push(item)
      return state;
    },
  },
}

I then set up the main Vuex store as follows:

import Vue from 'vue/dist/vue.esm';
import Vuex from 'vuex';
import OrderItemStore from './stores/order_item_store';

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    OrderItemStore,
  },
});

export default store;

In case you’re wondering why I’m using Vuex modules, it’s because I’m using several of them for things such as food articles and I wanted to keep things brief for this story.

The structure of my components looked like the following:

<Root>
  <PointOfSale>
    <Menu>
    <Checkout>
      <ShoppingCart>
      <Total>
      <Submit>

This works splendidly for the cashier-side of the app.

The customer basket page

I was able to reuse some of the components so that the user could look at their cart in a new Vue app running on a separate page:

<Root>
  <CustomerBasket>
    <ShoppingCart>
    <Total>

Setting up the shared mutations

Well wouldn’t ya know it, it turns out I can do everything I need thanks to the vuex-shared-mutations package (massive props to the devs behind this!)

In case you’re unfamiliar with them, Vuex handles updates to its stores using mutations, meaning that every change to the data in the app would be performed as a single action that mutates the data.

Let’s take another look at the OrderItemStore, specifically its mutations:

reset(state) {
  state.orderItems = []
  return state;
},
add(state, item) {
  state.orderItems.push(item)
  return state;
},

Here we have two mutations that can take place on the order items: one to reset them to a new state, and another to add a single item. These are the ones we’ll be handling!

In order to setup vuex-shared-mutations, I first had to add the package to the app:

$ yarn add vuex-shared-mutations

Great! Now I can access it from my store and roll it out.

import Vue from 'vue/dist/vue.esm';
import Vuex from 'vuex';
import createMutationsSharer from "vuex-shared-mutations";
import OrderItemStore from './stores/order_item_store';
Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    OrderItemStore,
  },
  plugins: [
    createMutationsSharer({
      // Right, so..
    }),
  ]
});

export default store;

Here’s how it looks in the official documentation:

import createMutationsSharer from "vuex-shared-mutations";

const store = new Vuex.Store({
  // ...
  plugins: [createMutationsSharer({ predicate: ["mutation1", "mutation2"] })]
});

After some poking around, I was able to adapt this to my store:

import Vue from 'vue/dist/vue.esm';
import Vuex from 'vuex';
import createMutationsSharer from "vuex-shared-mutations";
import OrderItemStore from './stores/order_item_store';
Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    OrderItemStore,
  },
  plugins: [
    createMutationsSharer({
      predicate: [
        'OrderItemStore/reset',
        'OrderItemStore/add',
      ]
    }),
  ]
});

export default store;

I ended up guessing that I could declare my store module name as a parameter (maybe it’s a given, but I’m still learning Vuex 😊), so I ended up creating a pull request with an example of this for the README, just in case.

Going back to the browser, and opening both pages on separate windows, it worked! Adding an OrderItem will show on both pages.

Couldn’t the screens get out of sync?

This is a very valid point. You might’ve noticed that this solution synchronizes mutations, rather than the store itself.

Of course, showing erroneous data to the user is something we don’t want. After some tests, it is of course possible to have these be out of sync.

I decided to leave it, however, for a few reasons:

  • The customer’s view would have to be closed in order for the desync to happen
  • The transactions are quick enough for the cart to be refreshed, and most importantly:
  • As long as the data on the cashier view, where the mutations are being triggered, is valid, then we’re mostly good

So there you have it

We can now have data across more than one browser tab. And yes, that totally means more than two, even! Once again, big thanks to the developers of the package.