destroytoday.com

Reactive time with Vue.js

Now that we’re diving into the day-to-day with time-tracking in Cushion, accurately indicating the current time and date is very important. As a timer runs, its duration should increase, but it should also grow in the graph as time passes. Likewise, as the current day carries over to the next day, the interface should update to reflect that today of a moment ago is now yesterday. Before reactive frameworks like Vue.js, this was a considerable task, but now it couldn’t be easier.

When we first released the time-tracking beta, we used a computed now property to indicate the current time:

{
  computed: {
    now () {
      return new Date;
    }
  }
}

We quickly realized that new Date is not reactive, so the computed property would cache itself and never update. Having a now property is still incredibly useful when dealing with time-tracking, but we needed to update the value on an interval, so the property wouldn’t remain cached. To achieve this, we used the setInterval function to update the now property each minute:

{
  data () {
    return {
      now: new Date,
    }
  },
  created () {
    setInterval(() => this.now = new Date, 1000 * 60);
  },
}

This worked well for a single component, but we rely on the now property in several other areas of the app, so we would need to update those values, too. I scoff at the idea of repeating this logic wherever needed, let alone running multiple intervals simultaneously, so we needed to find a better way. We needed a centralized now property that would update itself across all the components that rely on it.

In Cushion, we use Vue.js’s state management library, Vuex, which provides the centralized store we need to map global properties to our components. We moved the now property out of our component’s local state and into a brand new, rent-controlled time module within our Vuex store. This let us access the property from any component that needs to reference it instead of copy/pasting the interval everywhere like a monster.

We then added a start action to the time module, which starts the interval and commits the current time each minute—exactly like before, but now centralized:

const state = {
  now: new Date,
};

const actions = {
  start ({ commit }) {
    setInterval(() => { commit('updateTime') }, 1000 * 60);
  },
}

const mutations = {
  updateTime (state) {
    state.now = new Date;
  }
}

In our components, we map the centralized now state to a local computed property, which updates itself when the property changes:

{
  computed: {
    ...mapState('time', ['now']),
  }
}

And, for components that only need to be updated when the day changes, we can add a getter to our Vuex module:

import startOfDay from 'date-fns/start_of_day';

const getters = {
  today (state) {
    return startOfDay(state.now);
  }
}

Cushion’s interface will now automatically update itself to accurately reflect the current time. In the time-tracking week view, today’s column will highlight the current day and move to the next one when we cross midnight. In the day view, the “now” label will move across the graph as each minute passes and any timer will follow along with it. I doubt anyone will notice or think about the additional work required to make this happen, but that’s the story of my life.