I am trying to add some delay to transitioning out of an element when the value - v-if directive binds to - changes from true to false. I am using Vuex to maintain the state of isLoading so that it is used in other components.
So I have an API call that sets this.$store.state.isLoading to true while waiting for response, and false once response is received. But the problem is that API response is almost instant and the progress bar only flashes for a split second.
<template>
<div>
<b-progress v-if="isLoading" :max="max">
<b-progress-bar :value="count"></b-progress-bar>
</b-progress>
</div>
</template>
<script>
module.exports = {
data() {
return {
count: 0,
max: 100
}
},
computed: {
isLoading () {
return this.$store.state.isLoading;
}
}
}
</script>
Is this the right thinking by adding delay after the state is changed? If so, what is the proper way to do it?
You should add the delay right after your API call, assuming you have a mutation named updateIsLoading to update the isLoading state.
fetch(url)
.then(res => res.json())
.then(res => {
// do something with your data
setTimeout(() => this.$store.commit('updateIsLoading'), 1000)
})
.catch(...)
If you need to set the delay once and for all, one solution is to create an action, and then dispatch the action after you get response from the API call.
actions: {
updateIsLoading({ commit }) {
setTimeout(() => commit('updateIsLoading'), 1000)
}
}
If you really want to do this in your component, you can setup a watcher and update a local isLoading variable after some delay:
export default {
data() {
return {
count: 0,
max: 100,
isLoading: false
}
},
computed: {
loading () {
return this.$store.state.isLoading;
}
},
watch: {
loading(newVal, oldVal) {
setTimeout(() => this.isLoading = newVal, 1000)
}
}
}
Related
I am trying to return a function into computed property but on page refresh getter or state does not load the data into computed property. How can I resolve this ? I did try async await into computed property too it doesn't work. Please guide.
export default {
data(){
return {
isLoading: false,
}
},
async created(){
await this.profile()
},
methods: {
async profile(){
this.isLoading = true;
return await Promise.all([
this.$store.dispatch('class'),
this.$store.dispatch('list')
]).finally(() => {
this.isLoading = false;
})
}
},
computed: {
getItem() {
console.log(this.$store.getters); //This records did not load at first time after rerouting it does work
return () => this.$store.getters.listItem;
}
}
}
I can't figure out why you need to return a function from the computed value.
However, using the computed value in your template will work.
<template>
<div>
{{ getItem() }}
</div>
</template>
But if you want to see your console log, you can use a local variable to force the Vue to watch changes.
computed: {
getItem() {
const lst = this.$store.getters.listItem;
console.log(lst);
return () => lst;
},
},
I think it is better to use the store value directly.
computed: {
getItem() {
return this.$store.getters.listItem;
},
},
I want to make a special component which handles failed fetch requests. It is expected to work in this way:
If fetch request fails then several more attempts should be made after several seconds.
This special component should display countdown timer for next request to launch.
So I have:
Fetch function is in store. It works fine (makes 3 requests after 3, 6 and 9 seconds).
import { createStore } from "vuex";
const wait = async (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
export default createStore({
state: {
error: {
isError: false,
timerSec: null
}
},
mutations: {
setError(state, payload) {
state.error = payload.error;
}
},
actions: {
async fetchProducts({ commit, dispatch }, attempt = 1) {
try {
const response = await fetch("https://fakestoreapi.com/products222");
if (!response.ok) {
throw new Error("Something went wrong");
}
} catch (e) {
console.log("Request:", attempt);
commit("setError", {
error: {
isError: true,
timerSec: attempt * 3
}
});
if (attempt >= 3) {
return;
}
await wait(attempt * 3000);
return dispatch("fetchProducts", attempt + 1);
}
}
}
});
I call fetchProducts() in App.vue on mount. In App.vue I pass following data to error-request component:
<template>
<error-request v-if="error.isError" :timeout="error.timerSec"></error-request>
<h1 v-else>This should be rendered if there's no errors</h1>
</template>
In the error-request component I have a countDown method which triggered when timeout props changes.
<template>
<div>
<h1>The next attempt to fetch data will be made in:</h1>
<h2>{{ timer }}</h2>
</div>
</template>
<script>
export default {
props: ["timeout"],
data() {
return {
timer: null,
interval: null,
};
},
methods: {
countDown(sec) {
this.interval = setInterval(() => {
this.timer = sec;
if (sec === 0) {
clearInterval(this.interval);
return;
}
sec--;
}, 1000);
},
},
watch: {
timeout() {
this.countDown(this.timeout);
},
},
};
</script>
Unfortunatelly countdown timer shows only once and only on second request (it ignores first request with countdown from 3 to 1 and ignores third request. Could you help me to fix it?
I made a codesandbox: https://codesandbox.io/s/peaceful-sinoussi-ozjkq8?file=/src/App.vue
You should do:
methods: {
countDown(sec) {
this.timer = sec;
this.interval = setInterval(() => {
this.timer--;
if (this.timer === 0) {
clearInterval(this.interval);
return;
}
}, 1000);
},
},
watch: {
timeout: {
handler() {
this.countDown(this.timeout);
},
immediate: true,
},
},
There are 2 points to notice:
Don't modify the function parameters to prevent side effects (In your case is the sec parameter)
You have to trigger the watch for the first time so you need to add the option immediate: true
I have this function return a call back as:
function fetchShifts(ctx, callback) {
const accountId = selectedAccount.value.value.id
store.dispatch('app-action-center/fetchShifts', {
accountId,
})
.then(shifts => {
const data = []
shifts.forEach(async (shift, index) => {
const user = await store.dispatch('app-action-center/fetchUserDetails',
{
assignedTo: shift.assignedTo,
})
.then(res => res)
data.push({
...shift,
user: user.fullName,
})
if (index === (shifts.length - 1)) { callback(data) }
})
})
}
In the vue file I try to set it as:
data() {
return {
shifts: this.fetchShifts,
}
},
or
data() {
return {
shifts: null,
}
},
created() {
this.shifts = this.fetchShifts()
}
None of them work, I want to make this shifts variable ready when the component mounted so I can put it in the <app v-for="shift in shifts" />
At this moment, this code work fine with <b-table :items="fetchShifts /> but I don't know how to convert to <ul v-for="shift in shifts></ul>
Try like this:
<ul v-for="shift in shifts" :key="shift.id">
</ul>
export default
{
data()
{
return {
shifts: [],
};
},
created()
{
this.fetchShifts(undefined, (shiftsArray) =>
{
this.shifts = shiftsArray;
});
}
}
Explanation - initially you start with an empty array. Then you asynchronously fetch the shifts. The callback is called as soon as all the shifts and the corresponding users have been fetched - and in this callback you update the array with the shifts, which in turn triggers component re-rendering.
Vue is truly amazing!
I have two functions in my store, one that gets data by calling API and one that toggles change on cell "approved". Everything working fine, except that when I toggle this change it happens in database and I get the response that it is done but It doesn't update on UI.
I am confused, what should I do after toggling change to reflect change on UI, should I call my API from .then or should I call action method responsible for getting data from server.
export default {
state: {
drivers: {
allDrivers:[],
driversError:null
},
isLoading: false,
token: localStorage.getItem('token'),
driverApproved: null,
driverNotApproved: null
},
getters: {
driversAreLoading (state) {
return state.isLoading;
},
driverError (state) {
return state.drivers.driversError;
},
getAllDrivers(state){
return state.drivers.allDrivers
}
},
mutations: {
getAllDrivers (state) {
state.isLoading=true;
state.drivers.driversError=null;
},
allDriversAvailable(state,payload){
state.isLoading=false;
state.drivers.allDrivers=payload;
},
allDriversNotAvailable(state,payload){
state.isLoading=false;
state.drivers.driversError=payload;
},
toggleDriverApproval(state){
state.isLoading = true;
},
driverApprovalCompleted(state){
state.isLoading = false;
state.driverApproved = true;
},
driverApprovalError(state){
state.isLoading = false;
state.driverError = true;
}
},
actions: {
allDrivers (context) {
context.commit("getAllDrivers")
return new Promise((res,rej)=>{
http.get('/api/admin/getAllDrivers').then(
response=>{
if (response.data.success){
let data=response.data.data;
data=data.map(function (driver) {
return {
/* response */
};
});
context.commit("allDriversAvailable",data);
res();
}else {
context.commit("allDriversNotAvailable",response.data)
rej()
}
})
.catch(error=>{
context.commit("allDriversNotAvailable",error.data)
rej()
});
});
},
toggleDriverApproval (context, payload){
return new Promise((res, rej)=>{
http.post("/api/admin/toggleDriverApproval",{
driver_id: payload
})
.then( response => {
context.commit('driverApprovalCompleted');
res();
}).catch( error =>{
context.commit('driverApprovalError');
rej()
})
})
}
}
}
and here is the code on the view, I wrote the necessary code for better clarification of the problem
export default {
name: 'Drivers',
data: () => ({
data: [],
allDrivers: [],
driversErrors: []
}),
created() {
this.$store
.dispatch('allDrivers')
.then(() => {
this.data = this.$store.getters.getAllDrivers
})
.catch(() => {
this.errors = this.$store.getters.driverError
})
},
computed: {
isLoading() {
return this.$store.getters.driversAreLoading
}
},
methods: {
verify: function(row) {
console.log(row)
this.$store.dispatch('toggleDriverApproval', row.id).then(() => {
this.data = this.$store.getters.getAllDrivers
console.log('done dis')
})
},
},
}
if I understand your issue, you want the UI displaying your data to change to the updated data after making a post request.
If you are using Vuex you will want to commit a mutation, and use a getter display the data.
I am not sure how your post request is being handled on the server but if successful typically you would send a response back to your front end with the updated data, and commit a mutation with the updated data.
Example:
Make a Post request
toggleDriverApproval (context, payload){
return new Promise((res, rej)=>{
http.post("/api/admin/toggleDriverApproval",{
driver_id: payload
})
.then( response => {
context.commit('driverApprovalCompleted', response.data);
res();
}).catch( error =>{
context.commit('driverApprovalError', error.response.data);
rej()
})
})
}
If succesful commit the mutation
.then( response => {
context.commit('driverApprovalCompleted', response.data);
res();
})
response.data being your data you want to mutate the state with.
Mutation Example:
customMutation(state, data) {
state.driverApproval = data
}
Getter Example:
driver(state) {
return state.driverApproval
}
displaying the getter in a template
<template>
<div v-if="driver">{{driver}}</div>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
name: Example,
computed: {
driver() {
return this.$store.getters.driver
},
// or use mapGetters
...mapGetters(['driver'])
}
}
</script>
more examples can be found at Vuex Docs
For such component
<template>
<div>
<router-link :to="{name:'section', params: { sectionId: firstSectionId }}">Start</router-link>
</div>
</template>
<script lang="ts">
import { mapActions } from "vuex"
export default {
mounted() {
this.getSectionId()
},
computed: {
firstSectionId() {
return this.$store.state.firstSectionId
}
},
methods: mapActions(["getSectionId"])
}
</script>
Store:
const store: any = new Vuex.Store({
state: {
firstSectionId: null
},
// actions,
// mutations
})
I have a web request in the getSectionId action and it asynchronously fetches data and calls a mutation that will fill firstSectionId in state. During the initial rendering firstSectionId is null and I get the warning that a required parameter is missing during rendering of router-link.
It is not a problem here to add v-if="firstSectionId". But in general what is the approach for fetching data from a server to be displayed? Currently all my components are checking if there is data present in the store before rendering, is it normal or is there a better way to wait for data to be loaded before rendering it?
One approach for asynchronously fetching data is to use promise in vuex store actions.
Vue.http.get(API_URL)
.then((response) => {
//use response object
})
.catch((error) => {
console.log(error.statusText)
});
To demonstrate that I make request to this route. You can see how response should looks like. Let's save response object in state.users array.
store.js
const store = new Vuex.Store({
state: {
users: []
},
mutations: {
FETCH_USERS(state, users) {
state.users = users
}
},
actions: {
fetchUsers({ commit }, { self }) {
Vue.http.get("https://jsonplaceholder.typicode.com/users")
.then((response) => {
commit("FETCH_USERS", response.body);
self.filterUsers();
})
.catch((error) => {
console.log(error.statusText)
});
}
}
})
export default store
You noticed that there is self.filteruser() method after commit. That is crucial moment. Before that we are committing a mutation, which is synchronous operation and we are sure that we will have our response in store.state that can be used in filterUsers() method (don't forget to pass self parm)
Users.vue
import store from "../store/store"
export default {
name: 'users',
created() {
this.$store.dispatch("fetchUsers", { self: this })
},
methods:{
filterUsers() {
//do something with users
console.log("Users--->",this.$store.state.users)
}
}
}
Better ways (ES6 & ES7)
ES6 Promises for asynchronous programming
//User.vue
created() {
this.$store.dispatch("fetchUser").then(() => {
console.log("This would be printed after dispatch!!")
})
}
//store.js
actions: {
fetchUser({ commit }) {
return new Promise((resolve, reject) => {
Vue.http.get("https://jsonplaceholder.typicode.com/users")
.then((response) => {
commit("FETCH_USERS", response.body);
resolve();
})
.catch((error) => {
console.log(error.statusText);
});
});
}
}
ES7: async/await
To get away from callback hell, and to improve asynchronous programming use async function, and you can await on a promise. Code looks much easier to follow (like it is synchronous), but code isn't readable for browsers so you'll need Babel transpiler to run it.
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // wait for actionA to finish
commit('gotOtherData', await getOtherData())
}
}
In my experience, you can skip a few checks if you preset the state with an empty value of the same type as the expected result (if you know what to expect, of course), e.g. if you have an array of items, start with [] instead of null as it won't break v-for directives, .length checks and similar data access attempts.
But generally, adding v-if is a very normal thing to do. There's a section about this in the vue-router documentation and checking whether properties exist or not is exactly what it suggests. Another possible solution it mentions is fetching data inside beforeRouteEnter guard, which assures you will always get to the component with your data already available.
Ultimately, both solutions are correct, and the decision between them is more of a UX/UI question.
I had similar requirements for locations and the google map api. I needed to fetch my locations from the API, load them in a list, and then use those in a map component to create the markers. I fetched the data in a Vuex action with axios, loaded that in my state with a mutation, and then used a getter to retrieve the resulting array in the mounted life cycle hook. This resulted in an empty array as mounted fired before the async action resolved.
I used store.subscribe to solve it this way:
<template>
<div class="google-map" :id="mapName"></div>
</template>
<script>
import GoogleMapsLoader from 'google-maps';
import { mapGetters } from 'vuex';
export default {
name: 'google-map',
props: ['name'],
computed: {
...mapGetters({
locations: 'locations/locations',
}),
},
data() {
return {
mapName: `${this.name}-map`,
};
},
mounted() {
this.$store.subscribe((mutation, state) => {
if (mutation.type === 'locations/SAVE_LOCATIONS') {
GoogleMapsLoader.KEY = 'myKey';
GoogleMapsLoader.load((google) => {
/* eslint-disable no-new */
const map = new google.maps.Map(document.getElementById('locations-map'));
// loop through locations and add markers to map and set map boundaries
const bounds = new google.maps.LatLngBounds();
// I access the resulting locations array via state.module.property
state.locations.locations.forEach((location) => {
new google.maps.Marker({
position: {
lat: location.latitude,
lng: location.longitude,
},
map,
});
bounds.extend({
lat: location.latitude,
lng: location.longitude,
});
});
map.fitBounds(bounds);
});
}
});
},
};