Here's the code. No errors or anything, but the output doesn't update as expected.
Code in Vue SFC Playground
<script setup >
import { ref, reactive } from 'vue'
const arr = reactive([2,4,6])
setTimeout(()=> { arr.value = [1, 3, 5, 7] }, 500) // This doesn't work!
</script>
<template>
<h2 v-for="v in arr"> val {{ v }}</h2>
</template>
If you are using reactive, you get back an object where you can set its properties without value. You either define an object where arr is a property:
const state = reactive({
arr: [2, 4, 6]
})
setTimeout(()=> {state.arr = [1,3,5,7]}, 500)
Or for arrays I'd suggest using ref instead, then the rest of your code should work:
const arr = ref([2, 4, 6])
setTimeout can be pretty tricky regarding its context, and reactive also have its quirks.
Hence I do recommend using the following
<script setup>
import { useTimeoutFn } from "#vueuse/core"
import { ref } from "vue"
let arr = ref([2, 4, 6])
const { start } = useTimeoutFn(() => {
arr.value = [1, 3, 5, 7]
}, 500)
start()
</script>
<template>
<h2 v-for="v in arr" :key="v">val {{ v }}</h2>
</template>
useTimeoutFn is an official helper "hook" maintained by the Vue team.
The VueUse project is plenty of super useful functions like that, give it a try!
Related
Goal
I'm using Vue3 with the Composition API. I want to stream real-time data into a chart / graph (60 fps, possibly multiple streams / multiple updating arrays). To simplify for this question, I want to create a bar chart that updates it bars reactively to data changes by a button.
Attempts
I tried various JavaScript chart libraries like PrimeVue (Chart.js under the hood), vue-chartjs,Vue-ECharts, plotly.js. However, I'm still struggling either getting it to work or getting smooth animation. I thought real-time plotting would be a more common case, but I have a hard time finding examples (or my Google-foo fails me).
PrimeVue attempt
My best progress is with PrimeVue. I got the reactive part to work, but the issue is that the whole graph animates from scratch at each data update.
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import PrimeVue from 'primevue/config';
import Chart from 'primevue/chart';
const app = createApp(App);
app.use(PrimeVue);
app.component('Chart', Chart);
app.mount("#app");
<script setup lang="ts">
import { reactive, ref } from 'vue'
const chartValues = reactive([0.3, 1])
const basicData = ref({
labels: ['January', 'February'],
datasets: [
{
label: 'My First dataset',
backgroundColor: '#42A5F5',
data: chartValues
},
{
label: 'My Second dataset',
backgroundColor: '#FFA726',
data: [0.4, 0.1]
}
]
});
const horizontalOptions = ref(
{
// animation: {
// duration: 0
// }
}
);
function increment() {
chartValues[0] += 0.1
if (chartValues[0] > 1) {
chartValues[0] = 0
}
}
</script>
<template>
<h1>PrimeVue chart 2</h1>
<button #click="increment">count is: {{ chartValues[0] }}</button>
<div>
<div class="card">
<h5>Horizontal</h5>
<Chart type="bar" :data="basicData" :options="horizontalOptions" />
</div>
</div>
</template>
I can prevent the whole graph animation by setting animation: {duration: 0}, but this does not look dynamic/fluid. There is no transition between values.
Questions
It would be helpful if you could:
(best) Share a code snippet / link to a demo (any chart/graph library is okay) that only updates bars in a bar chart for which the data has changed (or line point in a line chart) with a smooth transition (or anything related to a single array) using Vue3.
Recommend a good JS chart library for smooth real-time plot animation that works with Vue3's reactive() / ref().
Any other advice that would help me
It depends on what kind of changes you want to make.
If you only want to change existing data, then it's relatively easy. If you want to add additional data to the chart it gets quite a bit harder, but based on your example, you're looking to mutate the a specific data inside an array so I won't cover the later.
The problem appears to be that these libraries don't handle the reactive data. Whether you have a reactive in ref or just using reactive The data passed to chart.js loses it's reactivity. I haven't looked into any of them to see why, but seems like prime-vue, vue-chartjs, and #j-t-mcc/vue3-chartjs all lose reactivity. I suspect they might be watching reactivity at a higher level, and when arrays and objects are mixed within reactive, it doesn't work well
to get around it, you can can manually call the update of the chartjs component. To do that pass the component a ref and then call the update() method when you are making an update (or using a watch)
<script>
import { reactive, ref, watch } from "vue";
export default {
setup() {
const barGraph = ref(null); // define the $ref
const basicData = reactive({
labels: ["January", "February"],
datasets: [
{
label: "My First dataset",
backgroundColor: "#42A5F5",
data: [0.3, 1],
},
{
label: "My Second dataset",
backgroundColor: "#FFA726",
data: [0.4, 0.1],
},
],
});
const horizontalOptions = {
animation: {
duration: 400,
},
};
function increment() {
let val = basicData.datasets[0].data[0];
basicData.datasets[0].data[0] = ((val * 10 + 1) / 10) % 1;
// when the data changes and the barGraph $ref is not null
if (barGraph.value) {
// call update method
barGraph.value.chart.update();
}
}
return {
basicData,
horizontalOptions,
increment,
barGraph,
};
},
};
</script>
<template>
<button #click="increment">
count is: {{ basicData.datasets[0].data[0] }}
</button>
<div>
<div class="card">
<h5>Horizontal</h5>
<Chart
type="bar"
:data="basicData"
:options="horizontalOptions"
ref="barGraph"
/>
</div>
</div>
</template>
With the Vue-ApexCharts library and learning from Daniel's answer, I was able to get a reactive bar chart to work.
Install Vue-ApexCharts in Vue3:
npm install --save apexcharts
npm install --save vue3-apexcharts
main.ts
import { createApp } from 'vue'
import App from './App.vue'
import VueApexCharts from "vue3-apexcharts";
const app = createApp(App);
app.use(VueApexCharts);
app.mount("#app");
App.vue
<template>
<div id="app">
<button #click="increment">Increment bar 1</button>
<button #click="addSeries">Add data series</button>
<button #click="incrementExtra">Increment appended series data</button>
<VueApexCharts :options="chartData.options" :series="chartData.series"/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import VueApexCharts from 'vue3-apexcharts'
const seriesData1 = reactive([0.3, 1])
const seriesDataExtra = ref([0.1, 0.7])
const chartData = reactive({
series: [
{
name: 'My First series',
data: seriesData1
},
{
name: 'My Second series',
data: [0.4, 0.1]
}
],
options: {
chart: {
type: 'bar',
// https://apexcharts.com/docs/options/chart/animations/
animations: {
enabled: true,
easing: 'linear',
speed: 200,
animateGradually: {
enabled: false
},
dynamicAnimation: {
enabled: true,
speed: 150
}
}
},
xaxis: {
categories: ['January', 'February']
}
}
})
function increment() {
seriesData1[0] = ((seriesData1[0] * 10 + 1) / 10) % 1 // + 0.1
// chartData.series[0].data[0] = ((chartData.series[0].data[0] * 10 + 1) / 10) % 1
console.log(seriesData1)
}
function incrementExtra() {
seriesDataExtra.value = seriesDataExtra.value.map(element => ((element * 10 + 1) / 10) % 1)
console.log(seriesDataExtra)
}
function addSeries() {
console.log("Add extra series")
chartData.series.push({
name: 'My Next series',
data: seriesDataExtra
})
}
</script>
The above code can update a single bar's data, which triggers only an animation for that bar. This is done by:
Creating a reactive variable: const seriesData1 = reactive([0.3, 1])
With data: seriesData1 we assign this reactive array to the Chart
The first button triggers the function increment, which increments the first bar's data by 0.1: seriesData1[0] = ((seriesData1[0] * 10 + 1) / 10) % 1
Note that for step 3 we could have also directly updated the data in the Chart with chartData.series[0].data[0] = ((chartData.series[0].data[0] * 10 + 1) / 10) % 1 and this would give the same effect (this line of code also updates the variable seriesData1).
However, by updating the reactive variable seriesData1, we can separate data and the chart logic. You don't have to know how to assign data to the chart, but still get your reactive chart by just modifying the data.
As a bonus I also added a demonstration on how to:
Add an extra data stream / series to the plot with the function addSeries and
Reactively animate this new data by incrementing all values in a ref array using the function incrementExtra.
Modify the animation behavior so it updates all bars at the same time without a bouncy effect (default)
Demo video: https://i.imgur.com/e5a0y8Z.mp4
Question about Template Refs in vue
I'm taking a look at the vue documentation about "refs" and in the part where it explains about ref inside a v-for it gives the following example:
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
I can understand its use in
const itemRefs = ref([])
but I couldn't understand why the ref is also applied in
const list = ref([
/* ... */
])
In a sandbox it is possible to remove the ref from the "list constant" without harming the function, so what would be the real application inside this constant?
<script setup>
import { ref, onMounted } from 'vue'
// const with ref
const list = ref([1, 2, 3])
const itemRefs = ref([])
onMounted(() => {
alert(itemRefs.value.map(i => i.textContent))
})
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// const without ref
const list = ([1, 2, 3])
const itemRefs = ref([])
onMounted(() => {
alert(itemRefs.value.map(i => i.textContent))
})
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
Using the ref turns list into reactive variable. Whenever new item is appended or it is otherwise mutated, the other functions or template parts watching it get updated. In your example without ref, when you append a new item to the list, it won't be automatically rendered in the template.
I can understand your confusion and I assume you probably come from vue2 world. I can't recommend enough reading vue3 docs about reactivity.
These are two different types of refs:
itemRefs is an array of template ref. It's a reference to an html element / vue component from your component template. It's associated to the ref attribute on the element.
list is a "regular" ref. It adds reactivity over the array of values. Without it, Vue won't react to the value changes and won't update the rendered component.
Using the Vue 3 composition API, how can I return the computed value for the property, firstDigit? The keyword, this, in the computed property is undefined but when I leave this out, then I get the error fourDigits is not defined.
<script setup>
import { computed, reactive } from 'vue'
const input = reactive({
fourDigits: Array(1,2,3,4),
firstDigit: computed(() => {
return this.fourDigits[0] <===== `this` is undefined but if I leave `this` out, then `fourDigits` is undefined.
})
</script>
<template>
<div>
<pre>
{{JSON.stringify(input.firstDigit, null, 2)}}
</pre>
</div>
</template>
this is something else in composition API , try with:
firstDigit: computed(() => {
return input.fourDigits[0]
})
If I need to use state property to assign a value to another state property, I do it in onMounted() hook. Like this:
<script setup>
import { computed, reactive } from 'vue'
const input = reactive({
fourDigits: Array(1, 2, 3, 4),
firstDigit: computed(() => {
return 0; // just some default value
})
});
onMounted(() => {
input.firstDigit = input.fourDigits[0];
})
</script>
<template>
<div>
<pre>
{{ JSON.stringify(input.firstDigit, null, 2) }}
</pre>
</div>
</template>
Check if it will work for you. All the best!
I have problem with updating v-for when I push new data to playlistTracks array. I'm passing playlistTracks from App.vue through playlist.vue to trackList.vue and once I run add() function which pushes new object to playlistTracks but it doesn't get rendered to site. In Vue devtools I can see it got passed with new data to trackList.vue but v-for just doesn't update and render it on site.
App.vue
<script setup>
import Playlist from './components/playlist.vue';
let playlistTracks = [
{
name: 'name4',
artist: 'artist4',
album: 'album4',
id: 4,
},
{
name: 'name5',
artist: 'artist5',
album: 'album5',
id: 5,
},
];
function add() {
const track = {
name: 'newTrack',
artist: 'newArtist',
album: 'new Album',
id: 10,
};
playlistTracks.push(track);
}
</script>
<template>
<div>
<h1>Ja<span class="highlight">mmm</span>ing</h1>
<div class="App">
<button #click="add">Test add</button>
<div class="App-playlist">
<Playlist
:playlistTracks="playlistTracks"
/>
</div>
</div>
</div>
</template>
playlist.vue
<script setup>
import TrackList from './trackList.vue';
const props = defineProps({
playlistName: String,
playlistTracks: Array,
});
</script>
<template>
<div class="Playlist">
<input value="New Playlist" />
<TrackList :tracks="props.playlistTracks" :isRemoval="true" />
<button class="Playlist-save">SAVE TO SPOTIFY</button>
</div>
</template>
trackList.vue
<script setup>
import Track from './track.vue';
const props = defineProps({
tracks: Array,
isRemoval: Boolean,
});
let tracksList = props.tracks;
</script>
<template>
<div class="TrackList">
<Track
v-for="track in tracksList"
:track="track"
:key="track.id"
:isRemoval="props.isRemoval"
/>
</div>
</template>
It looks like you're using the Composition API for single file components. An important note is that in the official Vue.js documentation, there is important information regarding references to reactive data:
Reactive state needs to be explicitly created using Reactivity APIs. Similar to values returned from a setup() function, refs are automatically unwrapped when referenced in templates:
This means that your array is most likely not retaining reactivity, so even if the array itself successfully updates, Vue is not detecting these changes so it can know to re-render!
The examples they give for resolving this are as follows:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button #click="count++">{{ count }}</button>
</template>
const obj = reactive({ count: 0 })
From this, we can come to the likely conclusion that your solution will consist of two different changes. First, to your App.vue file, wrap the array in the reactive() call:
/*...*/
import { reactive } from vue;
let playlistTracks = reactive([
{
name: 'name4',
artist: 'artist4',
album: 'album4',
id: 4,
},
{
name: 'name5',
artist: 'artist5',
album: 'album5',
id: 5,
},
]);
/*...*/
Then, simply remove the unnecessary additional property in trackList.vue:
<script setup>
import Track from './track.vue';
const props = defineProps({
tracks: Array,
isRemoval: Boolean,
});
</script>
<template>
<div class="TrackList">
<Track
v-for="track in props.tracks"
:track="track"
:key="track.id"
:isRemoval="props.isRemoval"
/>
</div>
</template>
Disclaimer: I am inexperienced with single file components, and especially with the Composition API, so some testing of the above solution will be required.
To maintain reactivity in Vue 3 composition API you have to wrap your data in either ref or reactive otherwise Vue will not detect any changes. ref can be used for both primitive and non-primitive while reactive can only be used for non-primitive types. Just wrap your playlistTracks data in ref and the component should rerender on any change within playlistTracks.
import {ref} from 'vue';
const playlistTracks = ref([
{
name: 'name4',
artist: 'artist4',
album: 'album4',
id: 4,
},
{
name: 'name5',
artist: 'artist5',
album: 'album5',
id: 5,
},
])
What's the alternative of Vue.delete in the new Reactivity API of Vue 3?
Vue.delete and Vue.set aren't needed in Vue 3. With the new reactivity system using proxies, Vue is able to detect all changes to reactive data.
You can use JavaScript's delete operator:
delete obj[key]
Here's a Vue 3 demo removing and adding object properties with vanilla JavaScript:
const { createApp, reactive } = Vue;
const app = createApp({
setup() {
const obj = reactive({ a: 1, b: 2, c: 3 })
return { obj }
}
});
app.mount("#app");
<div id="app">
Object: {{ obj }}
<hr>
<template v-for="(item, key) in obj">
<button #click="delete obj[key]">Delete key {{ key }}</button>
</template>
<button #click="obj['z'] = 'Added'">Add key z</button>
</div>
<script src="https://unpkg.com/vue#next"></script>