I have a long api call in my vuex store and if I change the route before it's finished and dispatch the same action from another page with a quicker api call the first call will eventually overwrite the second call. The action looks like this:
async getData({ commit }, payload) {
try {
const params = {...};
const res = await axios.get(`/data`, {
params,
});
if (res.status === 200) {
commit("setData", res.data);
}
} catch (error) {
commit("setError", error.response);
}
},
Is there a way to prevent this from happening?
If you want to prevent the 'last to finish' async call from overwriting the state, you need to cancel any old actions so they don't complete.
In your example, since you are using axios, the easiest option is to use an AbortController.
In your action, attach the controller to the axios method via the signal param:
const controller = new AbortController()
let result = axios.get('/foo/bar', { signal: controller.signal })
Then when you need to cancel it (i.e before calling a new action, simply call):
controller.abort()
Related
I am using the expo-auth-session package to make a request to the Spotify API to get access tokens, then saving to AsyncStorage.
A save function that stores the token in AsyncStorage:
const save = async (token) => {
try{
AsyncStorage.setItem('access_token', token)
}
catch(error){
console.log(error)
}
}
A getItem function that gets the access token value from AsyncStorage, and sets that value to the spotifyAccessToken state
const [spotifyAccessToken, setSpotifyAccessToken] = useState('');
const getItem = async () => {
try{
const token = await AsyncStorage.getItem('access_token')
setSpotifyAccessToken(token);
}
catch(error){
console.log(error)
}
}
Using the useAuthRequest from expo-auth-session to make a request to Spotify API, the request code below works.
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
tokenEndpoint: "https://accounts.spotify.com/api/token"
};
const [request, response, promptAsync] = useAuthRequest({
// responseType: ResponseType.Token,
responseType: 'code',
clientId: client_id,
//clientSecret: client_secret,
scopes: ['user-read-recently-played'],
usePKCE: false,
redirectUri: REDIRECT_URI
}, discovery)
useEffect(() => {
if (response?.type === 'success'){
//console.log(response.params.code);
axios.request({
method: 'POST',
url: 'https://accounts.spotify.com/api/token',
headers: {
'content-type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${new Buffer.from(`${client_id}:${client_secret}`).toString('base64')}`,
},
data: {
grant_type: 'authorization_code',
code: response.params.code,
redirect_uri: REDIRECT_URI
}
}).then(res => {
save(res.data.access_token);
}).catch(err => {console.log(err)})
}
},
[response]);
A button that triggers the user to login using Spotify account, after authenticating, it redirects back to this component screen, however, I want the text below the button to be displayed from "Loading..." to the spotifyAccessToken immediately after it redirects to the component screen, but it wouldn't. After I re-run my application, the token is displayed, which means it was successfully stored in AsyncStorage, but didn't update the state immediately. How can solve this? Thanks.
const [spotifyAccessToken, setSpotifyAccessToken] = useState(null);
useEffect(()=>{
//clearTokens();
// console.log('storage: ' + getValueForfor('access_token'))
// console.log('state: ' + spotifyAccessToken)
getItem()
}, [spotifyAccessToken])
<Button title='login to spotify' onPress={() => promptAsync()}/>
{spotifyAccessToken != '' ? <Text> {spotifyAccessToken} </Text> : <Text> Loading... </Text>}
This might be happening if you are redirecting to the component with getItem too early: before the AsyncStorage is done saving the token. Due to this, at the initial render of the component(with getItem), AsyncStorage.getItem might be getting the old value of access_token and not the updated one.
To possibly fix this issue, try redirecting to the next component only after AsyncStorage.setItem promise is resolved completely. Something like this:
This is how your save function should look like: it should return a Promise value:
const save = async (token) => {
try{
await AsyncStorage.setItem('access_token', token)
}
catch(error){
console.log(error)
}
}
And redirect to the next component after the save return promise value is resolved:
...
).then(async (res) => {
await save(res.data.access_token);
// Redirect here, after save is resolved
})...
Answering the question you asked in the comments to this answer:
it's not working still, you said that the save function should return a promise value, where in the code should I put it
Using await for a Promise makes the function wait till the promise is resolved (here when setItem is done). You do not need to explicitly return a Promise value from the async function in this case. If you do not use await, the function will return prematurely (without waiting for the setItem promise). The setItem promise will still resolve concurrently just that your code wouldn't be able to know when it is resolved.
By using await for setItem here, you just propagate promise resolution to the calling function(here in the then(res => {...}) block).
In the then(res => {}) block you can either use await to wait for the save to complete before executing the next statement. Or use then/catch and add the next statement to execute after save is done in the then block.
Edit: As OP mentioned in the comments below, the redirection to the next component is done automatically. Well, in this case, setting the value in AsyncStorage and immediately getting it in the next component might not work as expected because of the above-mentioned reason.
First, you will need to check if the auto-redirection to the next component is really done after the axios request completes or before it, i.e. as soon as response?.type === 'success'. I am unable to understand why you have made the axios request after you already got success from auth request
If the redirection is happening before the axios request call then you might be able to access the token in the success condition itself:
if (response?.type === 'success'){
// Check if the token is available here?
console.debug(`Response = ${JSON.stringify(response)}`);
// If token is available here itself, then why is the axios request required?
// Save the token here itself...
// Use SessionStorage if required, implementation explained below in the answer
...
}
If you confirmed the above and the auto-redirection is really done after the axios request and NOT after response?.type === 'success' then:
You could use react-native-session-storage as volatile storage to set and get the token in the same session and use AsyncStorage in parallel to it to set and get the token in/from persistent memory.
So, the save function will look like this with SessionStorage:
import SessionStorage from 'react-native-session-storage';
...
const save = async (token) => {
try{
// Set token in SessionStorage as well to allow access to the value immediately
SessionStorage.setItem(`access_token`, token);
// Store token to AsyncStorage to persist it when the app closes.
await AsyncStorage.setItem('access_token', token);
}
catch(error){
console.log(error)
}
}
And getItem function will look like this:
import SessionStorage from 'react-native-session-storage';
...
const getItem = async () => {
try{
let token = await AsyncStorage.getItem('access_token');
// If the token is not yet set in Async Storage, fetch it from Session Storage
// If it's set in Async Storage, use that value
if(!token) // If it's null
token = SessionStorage.getItem('access_token');
setSpotifyAccessToken(token);
// Don't forget to clear both SessionStorage and AsyncStorage on logout!
}
catch(error){
console.log(error)
}
}
Why both storages?
AsyncStorage
-> to persist the token when the user re-opens the app.
SessionStorage
-> as an immediate way to R/W the value during the same session (gets cleared when the app closes).
Another solution:
Use ContextProvider, if your code structure allows it. Wrap the context over the next component to "listen" to token value state change from anywhere in the children components.
I joined a big/medium project, I am having a hard time creating my first redux-saga-action things, it is going to be a lot of code since they are creating a lot of files to make things readable.
So I call my action in my componentDidMount, the action is being called because I have the alert :
export const fetchDataRequest = () => {
alert("actions data");
return ({
type: FETCH_DATA_REQUEST
})
};
export const fetchDataSuccess = data => ({
type: FETCH_DATA_SUCCESS,
payload: {
data,
},
});
This is my history saga : ( when I call the action with this type, The function get executed )
export default function* dataSaga() {
// their takeEverymethods
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
}
This is what has to be called : ( I am trying to fill my state with data in a json file : mock )
export default function* fetchTronconsOfCircuit() {
try {
// Cal to api
const client = yield call(RedClient);
const data = yield call(client.fetchSomething);
// mock
const history = data === "" ? "" : fakeDataFromMock;
console.log("history : ");
console.log(history);
if (isNilOrEmpty(history)) return null;
yield put(fetchDataSuccess({ data: history }));
} catch (e) {
yield put(addErr(e));
}
}
And this is my root root saga :
export default function* sagas() {
// many other spawn(somethingSaga);
yield spawn(historySaga);
}
and here is the reducer :
const fetchDataSuccess = curry(({ data }, state) => ({
...state,
myData: data,
}));
const HistoryReducer = createSwitchReducer(initialState, [
[FETCH_DATA_SUCCESS, fetchDataSuccess],
]);
The method createSwitchReducer is a method created by the team to create easily a reducer instead of creating a switch and passing the action.type in params etc, their method is working fine, and I did exactly what they do for others.
Am I missing something ?
I feel like I did everything right but the saga is not called, which means it is trivial problem, the connection between action and saga is a common problem I just could not figure where is my problem.
I do not see the console.log message in the console, I added an alert before the try-catch but got nothing too, but alert inside action is being called.
Any help would be really really appreciated.
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
should be
yield takeEvery(FETCH_DATA_REQUEST, fetchTronconsOfCircuit);
I'm having the following issue and hope someone could help me on it:
Fetch is not working on the first load (nor on reloads). It only works when on the client-side (when I move between routes).
I've read that watchQuery could help but didn't understand why and how to use it.
<script>
export default {
async fetch() {
const userId = await this.$nuxt.context.store.state.auth.authUser.userId
await this.$store.dispatch('case/fetchMyCases', userId.uid)
await this.$store.dispatch('case/fetchMyPendingCases', userId.uid)
...
It doesn't work even if I import and use firebase/auth directly.
<script>
import * as firebase from 'firebase/app'
import 'firebase/auth'
export default {
async fetch() {
const userId = await firebase.auth().currentUser
await this.$store.dispatch('case/fetchMyCases', userId.uid)
await this.$store.dispatch('case/fetchMyPendingCases', userId.uid)
...
Does anyone have any tips for it? I'd really appreciate it.
Thanks!
After literally 3 days searching/testing, I finally found out why I was having this issue.
The problem was that I simply put async/await for fetch but didn't put async/await for the actions itself. Therefore, my getter (in computed) was getting the store state before the dispatches have been finished.
Thanks, everyone!
Warning: You don't have access of the component instance through this inside fetch because it is called before initiating the component (server-side).
async fetch({ store }) {
await store.dispatch('case/fetchMyCases')
await store.dispatch('case/fetchMyPendingCases')
}
If you need parameter:
async fetch({ store, params }) {
await store.dispatch('case/fetchMyCases', params.uid)
await store.dispatch('case/fetchMyPendingCases', params.uid)
}
I gave an example of id. The name of the parameter depends on the name of your page.
_id => params.id
_uid => params.uid
_slug => params.slug
...
Yes, You must put async/await on actions.
async automatically returns a promise
If you don't need the value, in this case, don't anything return.
export const Actions = {
async fetchUsers() {
// It will return automatically promise
await this.$axios.get('API')
}
}
// If you need returne value
// First way
export const Actions = {
async fetchUsers() {
// It will return promise and value
return await this.$axios.get('API')
}
}
// Second way
export const Actions = {
async fetchUsers() {
// It will return promise and value
const response = await this.$axios.get('API')
return response;
}
}
I have a few components that can be separate or on the same page. Each of these components uses the same Vuex state. Since they can each be used on other pages and still work, each of them dispatches a call to the same Vuex action which in turns calls a service that uses axios to get the JSON data.
All of this works great!
However, when I do have 2 (or more) of these components on a single page, that axios call gets called 1 time for each of the components. Initially, I went down the path of trying to see if data existed and get created a "last got data at" timestamp so I could just bypass the 2nd call. However, these are happening both on the components created event and are being essentially called at the same time.
So, enter debounce. Seems like the exact reason for this. However, when I implement it, it fails and is passing on to the next line of code and not awaiting. What am I doing wrong?
Agenda Component (one that uses the same state)
async created() {
await this.gatherCalendarData();
},
methods: {
async gatherCalendarData() {
await this.$store.dispatch('time/dateSelected', this.$store.state.time.selectedDate);
},
},
Month Component (another, notice they are the same)
async created() {
await this.gatherCalendarData();
},
methods: {
async gatherCalendarData() {
await this.$store.dispatch('time/dateSelected', this.$store.state.time.selectedDate);
},
},
The Action getting called
async dateSelected(context, data) {
let result = await getCalendarData(isBetween.date, context.rootState.userId);
await context.commit('SET_MONTHLY_DATA', { result: result.Result, basedOn: isBetween.date });
},
This getCalendarData method is in a service file I created to make api calls (below.)
This is the error that I receive (once for each component) that calls this action.
[Vue warn]: Error in created hook (Promise/async): "TypeError: Cannot read property 'Result' of undefined"
Which is referring to the 3rd line above: result: result.Result
API Service
const getCalendarData = debounce(async (givenDate, userId) => {
let response = await getCalendarDataDebounced(givenDate, userId);
return response;
}, 100);
const getCalendarDataDebounced = async (givenDate, userId) => {
let result = await axiosGet('/api/v2/ProjectTime/BuildAndFillCalendarSQL', {
givenDate: givenDate,
userID: userId,
});
return result;
};
Axios Wrapper
const axiosGet = async (fullUrl, params) => {
let result = null;
try {
let response = await axios.get(fullUrl, params ? { params: params } : null);
result = await response.data;
} catch(error) {
console.error('error:', error);
}
return result;
};
If I put console.log messages before, after and inside the getCalendarData call as well as in the getCaledarDataDebounced methods: (assuming just 2 components on the page) the 2 before logs show up and then the 2 after logs appear. Next the error mentioned above for each of the 2 components, then a single 'inside the getCalendarData' is logged and finally the log from within the debounced version where it actually gets the data.
So it seems like the debouncing is working in that it is only run a single time. But it appears that await call let result = await getCalendarData(isBetween.date, context.rootState.userId); is not truly Waiting.
Am I missing something here?
EDITS after Answer
Based on #JakeHamTexas' answer, my action of dateSelected is now (actual full code, nothing removed like above as to not confuse anything):
async dateSelected(context, data) {
console.log('dateSelected action');
let isBetween = isDateWithinCurrentMonth(data, context.state);
if (!isBetween.result) {
// The date selected is in a different month, so grab that months data
return new Promise(resolve => {
getCalendarData(isBetween.date, context.rootState.userId)
.then(result => {
console.log('inside promise');
context.commit('SET_MONTHLY_DATA', { result: result.Result, basedOn: isBetween.date });
context.commit('SET_SELECTED_DATE', isBetween.date);
context.commit('statistics/TIME_ENTRIES_ALTERED', true, { root: true });
resolve();
});
});
} else {
// The date selected is within the given month, so simply select it
context.commit('SET_SELECTED_DATE', data);
}
context.commit('CLEAR_SELECTED_TIME_ENTRY_ID');
},
And my API call of getCalendarData is now:
const getCalendarData = async (givenDate, userId) => {
console.log('getting calendar data');
let result = await axiosGet('/api/v2/ProjectTime/BuildAndFillCalendarSQL', {
givenDate: givenDate,
userID: userId,
});
return result;
};
The error is gone! However, it does not seem to be debouncing - meaning everything gets called 3 times. I would expect the dateSelected action to be called 3 times. But I would like to avoid the getting calendar data being called 3 times. If it helps, this is what the console looks like:
dateSelected action
getting calendar data
dateSelected action
getting calendar data
dateSelected action
getting calendar data
inside promise
inside promise
inside promise
You need to return a promise from your action. Returning a promise of undefined (which is what is currently happening) resolves immediately.
dateSelected(context, data) {
return new Promise(resolve => {
getCalendarData(isBetween.date, context.rootState.userId)
.then(result => {
context.commit('SET_MONTHLY_DATA', { result: result.Result, basedOn: isBetween.date });
resolve();
}
}
},
Additionally, a vuex commit does not return a promise, so it doesn't make sense to await it.
here is what i do, and i'am not realy sure its correct :
//store
async addUser({commit}) {
try {
const {data} = await apiService.addUser()
commit('SET_USER', data)
commit('SET_NOTIFICATION', {type:'success', message: 'user successfuly created'})
} catch (error) {
commit('SET_NOTIFICATION', {type:'error', message:error})
}
}
SET_USER(state, user) {
state.users.push(user)
}
//my component:
async addUser() {
this.isLoading = true
await this.$store.dispatch('updatePatient', this.form)
this.isLoading = false
}
is it legit ?
sometimes i think i would need more logic inside my component depending on the succes or rejected api request. Should i put all the logic in my actions ? like i do at the moment ?
Maybe should I add a status state for each actions, for example :
state {
users: []
postUserSuccess: null
postUserError: false
updateUserSuccess: null
updateUserError: false
// ...
}
and do what i want in the component with a computed property mapped to the store ?
What do you think ?
I don't know if it's a best practice but I let the components the exception handling. That method has its pros (you don't have to pollute the state with error management) and cons (you have to repeat the error management code for every action call).
All service calls will be made in actions
The state will only be set in mutations.
All service calls will return a promise with a resolve(data to load in the state) and a reject(message errors to present).
There will be an interceptor to reject the response in case there's a custom error (here you can put if the response has an error prop reject the response and send as an error the error prop, now you don't have to deconstruct the response in the action).
I'm going to give you a simplified example (I use axios, you can learn how to do it with the library that you use).
Actions in Vuex are asynchronous. So you don't need to try/catch them.
ApiService - Add User
const addUser = () => {
return new Promise((resolve, reject) => {
axios
.post(url, user)
.then(response => resolve(response.data))
.catch(error => reject(error));
});
};
store
async addUser({commit}) {
const data = await apiService.addUser();
commit('SET_USER', data);
return data;
}
if the promise in apiService.addUser is resolved the commit is going to be made if is rejected axios will return the promise and you can catch the error in the component that calls the action.
Component
async addUser() {
this.isLoading = true;
try {
await this.$store.dispatch('updatePatient', this.form);
} catch (error) {
// here goes the code to display the error or do x if there is an error,
// sometimes I store an errors array in the data of the component other times I do x logic
}
this.isLoading = false;
}
State
Your state will be cleaner now that you don't need to store those errors there.
state {
users: []
}