Search code examples
javascripthtmlvue.jsvuex

How to set timer in Vuex?


I just started learning Vue and Vuex. I am trying to set up a countdown timer that starts counting from 5 seconds when it is clicked. The logic has been placed in Vuex store for state management but I couldn't get the timer to run (it is stuck at 5 unchanged).

At parent (Home.vue)

<template>
  <div class="home">
    <Header 
    @change-game-button="changeGameButton"
     :gameInActive="gameInActive"
     :gameActive="gameActive"
    />
  </div>
</template>

<script>
import Header from '../components/Header'

export default {
  name: 'Home',
  components: {
    Header,
  },
  data() {
    return {
      gameInActive: true,
      gameActive: false
    }
  },
  methods: {
    changeGameButton() {
      console.log("Game button fired");
      this.gameInActive = !this.gameInActive;
      this.gameActive = !this.gameActive;
    }
  }
}
</script>

At component (Header.vue)

<template>
  <div class="header-container">
    <StartResetButton 
    @btn-click="$emit('change-game-button')"
    :buttonText="gameInActive ? 'Start' : 'Restart'"
    :buttonColor="gameInActive ? '#90ee90' : '#FED8B1'"
    @click="gameInActive? 'resetCounter' : 'startCounter'"
    />
  </div>
  <div class="initialInstruction" v-if="gameInActive">{{ initialInstruction }}</div>
  <div class="gameStartTimer" v-if="gameActive">{{ gameStartTimer }}</div>
</template>

<script>
import StartResetButton from "./StartResetButton";
import { mapActions, mapMutations, mapState } from "vuex";

export default {
  components: {
    StartResetButton,
  },
  props: {
      gameInActive: Boolean,
      gameActive: Boolean,
  },
  computed: {
      ...mapState(["gameStartTimer", "initialInstruction"])
  },
  methods: {
    //   ...mapMutations(["countDown"]),
      ...mapActions(["startCounter", "resetCounter"])
  },
  emits: ['change-game-button']
};
</script>

At the store (index.js)

import { createStore } from 'vuex'

export default createStore({
  state: {
    gameStartTimer: 5,
    initialInstruction: "Press Start to Begin"
  },
  mutations: {
    stopCounter(state) {
      state.gameStartTimer = 5
    },
    counter(state) {
      if (state.gameStartTimer > 0) {
        setTimeout(() => {
          state.gameStartTimer--;
        }, 1000)
      }
    }
  },
  actions: {
    startCounter(context) {
      context.commit('counter')
    },
    resetCounter(context) {
      context.commit('stopCounter')
    }
  },
  modules: {
  }
})

StartResetButton.vue

<template>
  <button 
  class="btn"
  @click="onClick()" 
  :style="{ background: buttonColor }">
    {{ buttonText }}
  </button>
</template>

<script>
export default {
  name: "StartResetButton",
  props: {
    buttonText: String,
    buttonColor: String,
  },
  methods: {
      onClick() {
          this.$emit('btn-click')
      }
  }
};
</script>

Any tips on why the timer wouldn't fire?


Solution

  • The problem is that this won't execute click handlers:

    @click="gameInActive? 'resetCounter' : 'startCounter'"
    

    v-on (@) value should be either a function that is used as event handler, or an expression that is evaluated on event. This expression doesn't result in resetCounter, etc function calls because typeof (this.gameInActive? 'resetCounter' : 'startCounter') === 'string'. It's preferable to provide a single handler that will make decisions instead of putting conditions into a template.

    Vuex supports promises, it's preferable to use promise control flow for asynchronous actions. setTimeout callback is executed once. If it's supposed to decrement multiple times, setTimeout should be used in a loop with await, or setInterval should be used instead.

    Mutations are synchronous and commonly fine-grained functions that access the state directly. There can be ones called decrementCount, startCounter, etc.

      state: {
        count: 5,
        counter: false
      },
      actions: {
        async startCounter({ state }) {
          state.counter = true;
          while (state.count > 0 && state.counter) {
            await new Promise(resolve => setTimeout(resolve, 1000));
            if (state.counter)
              state.count--;
          }
          state.counter = false;
        },
        resetCounter(context) {
          state.count = 5;
          state.counter = false;
        }
      },