Request with Axios. How to solve the async problem? - javascript

I have this _id.vue page on my Nuxt.js project:
<template>
<div class="wrapper">
<HeaderApp>
<DivHeaderMenu>
</DivHeaderMenu>
</HeaderApp>
<CenterContentDinamicFirmenistorieApp>
</CenterContentDinamicFirmenistorieApp>
<FooterApp>
</FooterApp>
</div>
</template>
<script>
//company_history
import axios from 'axios';
import HeaderApp from '~/components/HeaderApp';
import FooterApp from '~/components/FooterApp';
import CenterContentDinamicFirmenistorieApp from '~/components/CenterContentDinamicFirmenistorieApp'
import DivHeaderMenu from '~/components/DivHeaderMenu';
import Pixelperfect from '~/components/Pixelperfect';
export default{
async fetch ({ store, params, redirect, app}) {
return axios.get('http://seo-gmbh.eu/json/api_sunds.php?action=get_pages&url=company_history')
.then((res) => {
store.commit('company_history/init_data_for_firmenistorie', res.data);
})
},
async validate({store, params, redirect}) {
const urlData = store.state.company_history.dbFirmenstorie.dbFirmenistorieSortArrayData;
let resultArray = false;
for (let i = 0; i < urlData.length; i++) {
if(params.id === urlData[i].toString()){
return resultArray = urlData[i];
}
}
if(resultArray == false){
return redirect('/Firmenistorie');
}
},
head () {
return {
title: this.$store.state.company_history.dbFirmenstorie.dbFirmenistorieData.data.meta.title,
meta: [
{description: this.$store.state.company_history.dbFirmenstorie.dbFirmenistorieData.data.meta.description}
]
}
},
components:{
HeaderApp,
FooterApp,
CenterContentDinamicFirmenistorieApp,
DivHeaderMenu,
Pixelperfect
},
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
My task is to get a redirect when I get the 404th error on the dynamic page (_id). The whole implementation works fine if I go through nuxt-link (s) to similar pages - the 404th error works fine if I enter an incorrect URL in the address bar. But the problem appears if I'm already on a working page - I reload it. Instead of loading the same page again, I get the 404th error and redirect as a result. This happens because in this particular case I do not receive data from the store
My question is: How can I solve this (asynchronous, as I understand it) problem? (I tried everything that is possible - nothing helps).
My Vuex repository looks rather piled up - but just in case, I'll throw its code for a better understanding of the problem:
export const state = () => ({
dbFirmenstorie: {
dbFirmenistorieData: null,
dbFirmenistorieMaxYearData: null,
dbFirmenistorieMaxDetailsData: null,
dbFirmenistorieSortArrayData: [],
},
});
export const mutations = {
init_data_for_firmenistorie (state, uploadDbFirmenistorieData) {
state.dbFirmenstorie.dbFirmenistorieData = uploadDbFirmenistorieData;
state.dbFirmenstorie.dbFirmenistorieData.data.content_json = JSON.parse(state.dbFirmenstorie.dbFirmenistorieData.data.content_json);
state.dbFirmenstorie.dbFirmenistorieData.data.meta = JSON.parse(state.dbFirmenstorie.dbFirmenistorieData.data.meta);
for (let i = 0; i < state.dbFirmenstorie.dbFirmenistorieData.data.company_history_data.length; i++) {
if(state.dbFirmenstorie.dbFirmenistorieSortArrayData.indexOf( Number( state.dbFirmenstorie.dbFirmenistorieData.data.company_history_data[i].company_history_from_year )) == -1 ){
state.dbFirmenstorie.dbFirmenistorieSortArrayData.push(Number(state.dbFirmenstorie.dbFirmenistorieData.data.company_history_data[i].company_history_from_year));
}
if(state.dbFirmenstorie.dbFirmenistorieMaxYearData < Number(state.dbFirmenstorie.dbFirmenistorieData.data.company_history_data[i].company_history_from_year)){
state.dbFirmenstorie.dbFirmenistorieMaxYearData = Number(state.dbFirmenstorie.dbFirmenistorieData.data.company_history_data[i].company_history_from_year);
state.dbFirmenstorie.dbFirmenistorieMaxYearData = Number(state.dbFirmenstorie.dbFirmenistorieData.data.company_history_data[i].company_history_from_year);
state.dbFirmenstorie.dbFirmenistorieMaxDetailsData = state.dbFirmenstorie.dbFirmenistorieData.data.company_history_data[i];
}
}
function sortNumber(a, b) {
return b - a;
}
state.dbFirmenstorie.dbFirmenistorieSortArrayData.sort(sortNumber);
}
};

I am pretty sure that if you start using catch() with axios as everyone should always do, you will be able to handle all non 200 responses just fine. Which means 404, 40x, 50x, etc...
axios
.get("https://example.com")
.then(res => console.log(res))
.catch(e => console.log(e))

Related

Vue async call REST API in Vuex store

My async actions do not run correctly. Im new to Vue and JS and I am not sure what is happening here. I placed some confirm() dialogs within my code, to see which line passed and which not.
Within the ScanView.vue I call my addProduct action. I get the confirm dialog saying "addProduct" and dispatch calles the next callAPI action where I get the "callAPI" confirm dialog but nothing more. Seems like fetch() isnt working at all, because no any other dialog is shown. What am I doing wrong?
ScanView.vue
export default defineComponent({
name: "Home",
methods: {
scanEan() {
// QR Code Scanner Logic
this.$store.dispatch("addProduct", ean);
}
});
main.js
const store = new Vuex.Store({
state: {
products: [{
name: 'Produkt',
ean: '123',
amount: '1',
smallImageUrl: 'smImage',
mediumImageUrl: 'mdImage',
largeImageUrl: 'lgImage',
expiration: []
}]
},
mutations: {
addProduct(state, product) {
state.products.unshift(product);
}
},
actions: {
addProduct(context, ean) {
confirm("addProduct: " + ean);
context.dispatch('callAPI', ean);
},
callAPI(context, ean) {
confirm("callAPI: ");
fetch("https://world.openfoodfacts.org/api/v0/product/" + ean + ".json") //
.then(response => {
confirm("reesponse");
return response.json();
}
) //
.then(data => {
confirm("data: " + data);
context.dispatch('saveProduct', data);
});
},
saveProduct(context, data) {
confirm("saveProduct: ");
const name = data.product.product_name;
const ean = data.code;
const smImage = data.product.image_front_thumb_url;
const mdImage = data.product.image_front_small_url;
const lgImage = data.product.image_front_url;
const expiration = new Array();
const date = new Date(data.product.expiration_date);
expiration.push(date);
const product = new Product(
name,
ean,
smImage,
mdImage,
lgImage,
expiration
)
confirm("Produktdata: " + product);
context.commit('addProduct', product);
}
}
});
app.use(store);
EDIT
I build a simulate button for better testing. QR Scanning does not work in Browser.
Result It does work in Browser. But not on my emulator or android device. Seems like fetch() isnt the right way with ionic-vue. If I catch the error I got TypeError: Failed to fetch...
<template>
<button #click="simulateScan">Simulate Scan</button>
</template>
<script>
export default {
methods: {
simulateScan() {
this.$store.dispatch('addProduct', 737628064502);
}
}
};
</script>
Final Solution
fetch() does not work on android. You have to use something like cordova-http, capacitor-http, ionic-http or else. I used capacitorcommunity-http.
npm install #capacitor-community/http
npx cap sync
import { Http } from '#capacitor-community/http';
[...]
callAPI(context, ean) {
var eanurl = "https://world.openfoodfacts.org/api/v0/product/" + ean + ".json";
Http.get({ url: eanurl}) //
.then(response => {
return response.data;
}
) //
.then(data => {
console.log(data);
context.dispatch('saveProduct', data);
}).catch(error => confirm(error));
},
[...]

How to call a composition function from another composition in VueJS

I've been experimenting with the new composition-api in VueJS and am not sure how to solve a problem. I'm looking for some advice on how to properly implement a solution. This wasn't a problem when everything was vuex-based since you can dispatch an action to another module without a problem. However, I'm struggling to find a solution for the composition implementation.
Problem:
Component calls a CompositionA's function.
CompositionA triggers a login function.
On CompositionA's login success/failure response I would like to call a CompositionB function. (CompositionB contains data and logic for showing a snackbar that's used across the site)
The problem is that it is necessary to inject the snackbar dependency in every component rather than have it be instantiated/mounted from CompositionA. Current solution is to this effect:
Component.vue:
// template calls login(credentials) method
import { useCompositionA } from '#/compositions/compositionA'
import { useCompositionB } from '#/compositions/compositionB'
export default {
name: 'Component',
setup(props, context) {
const { login } = useCompositionA(props, context, useCompositionB(props, context))
return {
login
}
},
}
compositionA.js:
export const useAuth = (props, context, snack) => {
const login = async (credentials) => {
try {
return await loginWithEmailPassword(credentials)
snack.show({text: 'Welcome back!'})
} catch (err) {
snack.show({text: 'Failed to login'})
}
}
return { login }
}
compositionB.js:
export const useSnack = (props, context) => {
const snack = reactive({
color: 'success',
text: null,
timeout: 6000,
visible: true,
})
const snackRefs = toRefs(snack)
const show = ({ text, timeout, color }) => {
snackRefs.text.value = text
snackRefs.timeout.value = timeout || 6000
snackRefs.color.value = color || 'success'
snackRefs.visible.value = true
}
return {
...snackRefs,
show
}
}
Would be nice if something like below existed, but I'm finding that the properties aren't reactive in CompositionB if it's used from CompositionA (method gets called but snackbar doesn't show up). My understanding is that Vue isn't injecting CompositionB into the Component, so I'm just running another instance of CompositionB inside CompositionA. What am I doing something wrong? What's the proper solution here?
compositionA.js (not working):
import { useCompositionB } from '#/compositions/compositionB'
export const useAuth = (props, context) => {
const login = async (credentials) => {
const { show } = useCompositionB()
try {
return await loginWithEmailPassword(credentials)
show({text: 'Welcome back!'})
} catch (err) {
show({text: 'Failed to login'})
}
}
return { login }
}
Thanks in advance,
As expected it was due to the Component referencing its own local copy of CompositionB*. Solution is actually to bring the state of your compositions into the global scope according to:
https://vueschool.io/articles/vuejs-tutorials/state-management-with-composition-api/
Something like this:
compositionB.js:
const snack = reactive({
color: 'success',
text: null,
timeout: 6000,
visible: true,
})
export const useSnack = (props, context) => {
const snackRefs = toRefs(snack)
const show = ({ text, timeout, color }) => {
snackRefs.text.value = text
snackRefs.timeout.value = timeout || 6000
snackRefs.color.value = color || 'success'
snackRefs.visible.value = true
}
return {
...snackRefs,
show
}
}
Works like a charm.
Only caveat I found initially was a composition-api error:
Uncaught Error: [vue-composition-api] must call Vue.use(plugin) before using any function.
This was easily solved by mounting the composition-api first thing in main.js as per solution here:
Uncaught Error: [vue-composition-api] must call Vue.use(plugin) before using any function
I think this won't be a problem with vue3 comes out. Hope this helps someone.

JavaScript, Redux: array not taking value

I am trying to get some statistics and problems for a user using a Redux action and pass it to a React component. The problem is, I have the array of objects curPageExercisesMarked, which I use for the pagination of the page, but it does not take the values I assign it to.
The stranger thing is that the other fields in the Redux store get updated, but not this one. I tried consoling the object in the action, but it just prints this:
It is important to mention that I am doing something similar in another action, using the exact same assignment and it works there. I've lost already an hour trying to figure this thing out so any help is welcomed.
The Redux action:
export const setStatistics = (
problems,
problemsSolved,
filter = ''
) => dispatch => {
let payload = {
subject1: 0,
subject2: 0,
subject3: 0,
total: 0,
exercisesMarked: [],
curPageExercisesMarked: []
};
for (let i = 0; i < problems.length; i++) {
if (problems[i].S === '1' && problemsSolved.includes(problems[i]._id)) {
payload.subject1++;
payload.total++;
payload.exercisesMarked.push(problems[i]);
} else if (
problems[i].S === '2' &&
problemsSolved.includes(problems[i]._id)
) {
payload.subject2++;
payload.total++;
payload.exercisesMarked.push(problems[i]);
} else if (
problems[i].S === '3' &&
problemsSolved.includes(problems[i]._id)
) {
payload.subject3++;
payload.total++;
payload.exercisesMarked.push(problems[i]);
}
}
payload.curPageExercisesMarked = payload.exercisesMarked.slice(0, 10);
dispatch({
type: SET_USER_STATISTICS,
payload
});
};
The redux reducer:
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_USER_STATISTICS:
return {
...state,
exercisesMarked: payload.exercisesMarked,
curPageExercisesMarked: payload.curPageExercisesMarked,
subject1: payload.subject1,
subject2: payload.subject2,
subject3: payload.subject3,
total: payload.total
};
case CHANGE_PAGE_MARKED:
return {
...state,
page: payload,
curPageExercisesMarked: state.exercisesMarked.slice(
(payload - 1) * state.pages_count,
payload * state.pages_count
)
};
default:
return state;
}
}
This is the part that does not function:
payload.curPageExercisesMarked = payload.exercisesMarked.slice(0, 10);
EDIT
I've discovered that if I go a component which loads all the problems and come back to this component, it actually gets the correct value.
Now, the interesting is that I do get the same problems here as well. Is it the way I use React Hook?
This is the part where I call the redux action in the react component:
const Dashboard = ({
problems: { problems },
auth: { user },
getProblems,
dashboard: {
curPageExercisesMarked,
page,
exercisesMarked,
pages_count,
subject1,
subject2,
subject3,
total
},
setStatistics
}) => {
useEffect(() => {
if (problems === null) {
getProblems();
} else if (user !== null) {
setStatistics(problems, user.problemsSolved);
}
}, [problems, user]);
// rest of the code
}
You can first simplify code as below. Update/Print console.log(JSON.stringify(payload)). I think if(problemsSolved.includes(problems[i]._id)) not working as expected
export const setStatistics = (
problems,
problemsSolved,
filter = ""
) => dispatch => {
let payload = {
subject1: 0,
subject2: 0,
subject3: 0,
total: 0,
exercisesMarked: [],
curPageExercisesMarked: []
};
for (let i = 0; i < problems.length; i++) {
if(problemsSolved.includes(problems[i]._id)) {
payload["subject"+ problems[i].S]++
payload.total++;
payload.exercisesMarked.push(problems[i]);
}
}
payload.curPageExercisesMarked = payload.exercisesMarked.slice(0, 10);
dispatch({
type: SET_USER_STATISTICS,
payload
});
};
// Also
case SET_USER_STATISTICS:
return {
...state,
...payload
};

Asynchronously updating Zabuto Calendar

I have Zabuto calendar showing booking dates, but some tours have many dates for the period, and loading them is slow.
I have changed the API to paginate the data, and send a next url if there is more data to load, but I can't see how to get zabuto calendar to update its data once it is displayed, and with Javascript being the ultimate asynchronous programming language, I figured there must be a way the calendar can display and upload data at the same time.
Posts such as this
How to load data from ajax to zabuto calendar plugin?
shows how to load the calendar data by ajax call, but not how to continuously upload more data asynchronously while the current calender data is displayed. Other posts indicate that the only way is to reload the entire calendar
reloading AJAX data for Zabuto Calendar after modal dismissal. I would prefer an asynchronous way.
The previous developer started to use the Vue framework, So what I have is a Zabuto calendar Vue module
<template>
<div id="my-calendar-a"></div>
</template>
<script>
import Vue from 'vue'
import moment from 'moment'
import { mapGetters } from 'vuex'
export default {
name: 'ZabutoCalendar',
methods: {
initialise: function () {
$('.calendar-month-navigation .glyphicon').click(function () {
Vue.bus.$emit('calendar-change-month')
})
}
},
props: ['tour'],
computed: {
...mapGetters('cart', [
'cartItems'
]),
...mapGetters('calendar', [
'tourDates'
])
},
mounted: function () {
var self = this
var currentDate = new Date()
/*
Use of the thrid party plugin zabuto calendar to
set up the calendar and check if dates are being clicked
https://github.com/zabuto/calendar
*/
var nextUrl = '/api/check-dates?year=' + moment().format('YYYY') +
'&month=' + moment().format('M') + '&tour=' + self.tour;
this.$store.dispatch('calendar/getTourDates', nextUrl).then(response => {
// I tried putting a while nextUrl loop here, but the calender wont display till dispatch returns
nextUrl = self.tourDates[1].next_url;
$(self.$el).zabuto_calendar({
data: self.tourDates[0].tourdates,
weekstartson: 0,
show_previous: false,
year: currentDate.getFullYear(),
month: currentDate.getMonth() + 1,
action: function () {
if ($(this).find('> div').hasClass('start_spots')) {
// reconstruct data
var selectedTour = {}
var id = this.id
var elem = $('#' + id)
$('.calendar-dow .selected').removeClass('selected')
$(this).find('> div').addClass('selected')
selectedTour = _.find(self.tourDates[0].tourdates, { 'tour_date_id': elem.data('tour_date_id') })
selectedTour['styled_date'] = moment(elem.data('date')).format('Do MMMM YYYY')
if ($(this).find('> div').hasClass('start_future')) {
selectedTour['available'] = 1
for (var index in self.cartItems) {
if (self.cartItems[index].date === elem.data('date')) {
selectedTour['available'] = 3
break
}
}
} else {
selectedTour['available'] = 2
}
self.$store.commit('calendar/setSelectedTour', selectedTour)
Vue.bus.$emit('date-click')
}
}
})
// while loop could surround above code
})
}
}
</script>
And a javascript module to do the ajax call to get all the data in one go
import axios from 'axios'
import moment from 'moment'
export const calendar_module = {
namespaced: true,
state: {
tourDates: [],
selectedTour: {}
},
getters: {
tourDates: (state) => {
return state.tourDates
},
selectedTour: (state) => {
return state.selectedTour
}
},
mutations: {
setSelectedTour (state, selectedTour) {
state.selectedTour = selectedTour
},
setTourDates (state, tourDates) {
state.tourDates = tourDates
}
},
actions: {
getTourDates ({ commit }, datesUrl) {
var response_data = axios.get(datesUrl).then((response) => {
commit('setTourDates', response.data)
});
return response_data;
}
}
}
The API response data is returned in the form
{ 'tourdates': array_data_object, 'next_url', url_string }
with next_url (within the response) set to an empty string if there is no more data. getTourDates actually returns the API response. I tried putting a while nextUrl loop around the code where commented, but zabuto calendar does not display till the dispatch function returns.
Does Zabuto Calendar have a built in way to asynchronously update its data while displaying? Otherwise how else can I get it to asynchronously display and load future dates?
Another way would be to get the ajax call to run several concurrently, and just return null in any that are redundant, but I would prefer to query the database first to see how many pages are needed, and would prefer not to waste an ajax call just to find out how many asynchronous hits are needed to get all data.
I could not put in a while nextUrl loop, so tried axios.all() instead, which allows for asynchronous calling of multiple gets at the same time. This made no improvement of load time, which was probably just as well, because it forced me to look at my REST API which had several inefficiencies that I would not have otherwise cleaned up.
This is my Async solution (which I no longer needed to use once the API was nice and quick) for posterity
import axios from 'axios'
import moment from 'moment'
export const calendar_module = {
namespaced: true,
state: {
tourDates: [],
selectedTour: {}
},
getters: {
tourDates: (state) => {
return state.tourDates
},
selectedTour: (state) => {
return state.selectedTour
}
},
mutations: {
setSelectedTour (state, selectedTour) {
state.selectedTour = selectedTour
},
setTourDates (state, tourDates) {
if (state.tourDates.length == 0) {
state.tourDates = tourDates[0].tourdates;
} else {
state.tourDates = state.tourDates.concat(tourDates[0].tourdates);
}
}
},
actions: {
getTourDates ({ commit }, datesUrl) {
var response_data = axios.all([
axios.get(datesUrl + '&page=1'),
axios.get(datesUrl + '&page=2'),
axios.get(datesUrl + '&page=3'),
axios.get(datesUrl + '&page=4'),
axios.get(datesUrl + '&page=5'),
axios.get(datesUrl + '&page=6')
]).then(axios.spread(function (response1, response2, response3, response4, response5, response6) {
commit('setTourDates', response1.data);
commit('setTourDates', response2.data);
commit('setTourDates', response3.data);
commit('setTourDates', response4.data);
commit('setTourDates', response5.data);
commit('setTourDates', response6.data)
}));
return response_data;
}
}
}

Redux updateElementSaga has been cancelled. Why?

I just implemented a drag and drop feature with react-dnd and when the user drops the SkyElement item in my app, I update top and left on the server which in turn updates the redux store
However, the update call works occasionally, not every time. And in my console, I see a warning; updateElementSaga has been cancelled
In my SlotView.js, in a function, I have:
this.props.dispatch(requestUpdateElement({ id, top, left }));
In my elements/actions.js:
export function requestUpdateElement(element) {
return { type: 'requestUpdateElement', element };
}
In my elements/sagas.js:
export function *updateElementSaga(action) {
const response = yield call(api.updateElement, action.element);
if (response.element) {
// debugger; // this hits, saga was cancelled will have appeared in the console at this point
yield put(actions.receiveElement(response.element));
} else if (response.error) {
console.log('error receiving element');
}
}
export default [
takeLatest('requestUpdateElement', updateElementSaga),
];
In api.js:
export function updateElement(element) {
const userId = JSON.parse(localStorage.cookies).userId;
element.userId = userId;
if (userId) {
return apiHelper.put(
`${apiHelper.getBaseUrl()}/users/${element.userId}/elements/${element.id}`,
{element},
{headers: apiHelper.getHeaders()}
).catch((error) => {
return {error};
});
} else {
console.log('user ID could not be found for request');
}
}
And my elements/reducer.js:
const defaultState = {
elementsMap: {},
visibleElements: [],
unplacedElements: [],
};
export default function(state = defaultState, action) {
switch (action.type) {
case 'receiveElement':
let element = null;
let unplacedElement = null;
if (action.element.sectorId === undefined) {
unplacedElement = `${action.element.id}`;
} else {
element = `${action.element.id}`;
// don't add, duplicate
const newState = {...state}; // copy old state
delete newState[`${action.element.id}`]; // delete the item from the object
const newVisibleElements = newState.visibleElements.filter(e => e !== `${action.element.id}`); // remove item from visible elements
const newUnplacedElements = newState.unplacedElements.filter(e => e !== `${action.element.id}`);
return {
...newState,
elementsMap: {
...newState.elementsMap,
[element]: action.element,
},
visibleElements: [...newVisibleElements, element],
unplacedElements: [...newUnplacedElements],
};
}
return {
...state,
elementsMap: {
...state.elementsMap,
[action.element.id]: action.element,
},
visibleElements: [...state.visibleElements, element],
unplacedElements: [...state.unplacedElements, unplacedElement],
};
default:
return state;
}
}
Like I mentioned before, sometimes the update works, but not every time. I've narrowed the problem down to the client. Server seems to be acting fine. Any idea what I'm doing wrong here? Thanks!
If you are using takeLatest the redux saga documentation does mention:
https://redux-saga.js.org/docs/basics/UsingSagaHelpers.html
Unlike takeEvery, takeLatest allows only one fetchData task to run at
any moment. And it will be the latest started task. If a previous
task is still running when another fetchData task is started, the
previous task will be automatically cancelled.
Where fetchData is the generator function that is being served using takeLatest or takeEvery
And when your UI keeps invoking the same action, before it gets completed, it will keep cancelling
the last invoked action, and hence you would keep getting the message intermittently:
updateElementSaga has been cancelled
Which by nature takeLatest is doing the right thing. Which is:
Always take the latest invoked action
In case you want every action to be caught and processed, do use takeEvery, as:
export default [
takeEvery('requestUpdateElement', updateElementSaga),
];

Categories