Search code examples
javascriptvue.jschart.jsnuxt.jsvuex

Chart.js in a Vue page throws error: do not mutate vuex store state outside mutation handlers


I have a Nuxt/Vue app with a page displaying a chart. This page also contains

  • a modal component that allows users to create a new data point
  • a dropdown menu that allows users to select how many days of data do they want the chart to display.

I would like to achieve the following:

  • Refresh the page (call my API again) after creating a new data entry
  • Refresh the page when the user chooses a different dropdown value

The main problem I'm facing now is that the barchart disappears a few seconds after I close the data entry modal and I can see the following error in the console: Error: [vuex] do not mutate vuex store state outside mutation handlers..

Additionally, I cannot seem to trigger an update by changing the dropdown value, but that is my secondary problem.

Since this example involves an external API, it is difficult to come up with a reproducible example, but here is what my setup looks like:

<template>
...
<select v-model="chartSelection", :oninput="refreshChart">
   <option value="0">7 days</option>
   <option value="1">14 days</option>
</select>
<bar-chart :data="chartData" :options="chartOptions"/>
<Modal v-if="showModal">
<!-- 
This is a custom component with its own modalToggle function.
It opens a form that allows you to create a new data point.
When this modal is closed, I would like to refresh the page
over which this modal appears
-->
...
</template>
...
<script>
async function getData(axios, store, period){
   const data = await axios.get(`my/endpoint/${period}`)
   const chartData = {
      labels: data.data.dates,
      datasets: [
        {backgroundColor: 'red', label: 'A', data.data.a},
        {backgroundColor: 'blue', label: 'B', data.data.b},
      ]
   }
   store.commit('saveChartData', chartData) // a simple assignment mutation in vuex
}
...
export default {
   async asyncData({$axios, store}) {
      await getData($axios, store, '7')
   },
   methods: {
      refreshChart() {
         return getData(this.$axios, this.$store, this.days)
      }
   },
   data() {
      return {
         chartSelection: '0', // this should determined how much data we fetch
      }
   }
},
computed: {
   ...mapState(['chartData', 'showModal']),
   days() {
      return this.chartSelection == '0' ? '7' : '14'
   }
}
...
</script>

If anyone has any suggestions for how to fix this, please let me know!

EDIT:

FYI, the modal does nothing but presents a form that submits chart data to the API. The bar-chart in the above example is a chart.js plugin that looks like this:

import Vue from 'vue'
import Bar from 'vue-chartjs'
const registerComponent = function (name, originalComponent) {
  Vue.component(name, {
    extends: originalComponent,
    props: ['data', 'options'],
    mounted() {
      this.renderChart(this.data, this.options)
    },
  })
}


Solution

  • Regarding the chart updates:

    When the data point is added in the modal, emit a custom event to the parent component, that will call getData.

    <Modal v-if="showModal" @dataPointAdded="getData">
    

    Within the Modal:

    AddDataPoint(data) {
       /* Add data point */
       this.$emit("dataPointAdded");
    }
    

    To update the chart component with the new data - add a watcher:

    props: ['data', 'options'],
    mounted() {
      this.renderChart(this.data, this.options)
    },
    watch: 
    {
        data() {
            this.$data._chart.update();
        },
        options() {
            this.$data._chart_update();
        }
    }
    

    See the docs here: https://vue-chartjs.org/guide/#troubleshooting

    Own Watcher