How to handle conditions after asynchronous request - javascript

Thanks for reading my question in advance. I'm using the dva and Ant Design Mobile of React handling phone register function.
Before sending the verify code, I will judge if the phone has been registered. If yes, it will Toast " This phone has been registered".
Now, the return value is correct:
const mapStateToProps = (state) => {
console.log(state.register.message)
}
// {code: 221, message: "This phone has been registered"}
So I write it as:
const mapStateToProps = (state) => ({
returnData: state.register.message
})
And then when I click the button, it will dispatch an action (send a request):
getVerifyCode() {
const { form, returnData } = this.props;
const { getFieldsValue } = form;
const values = getFieldsValue();
this.props.dispatcher.register.send({
phone: values.phone,
purpose: 'register',
})
// if(returnData.code === 221){
// Toast.fail("This phone has been registered", 1);
// } else {
// Toast.success("Send verify code successfully", 1);
// }
}
But when I tried to add the if...else condiction according to the return value
if(returnData.code === 221){
Toast.fail("This phone has been registered", 1);
} else {
Toast.success("Send verify code successfully", 1);
}
only to get the error:
Uncaught (in promise) TypeError: Cannot read property 'code' of
undefined
I supposed it's the problem about aynchromous and tried to use async await:
async getVerifyCode() {
...
await this.props.dispatcher.register.send({
phone: values.phone,
purpose: 'register',
})
}
But get the same error
Cannot read property 'code' of undefined
I wonder why and how to fix this problem ?
added: this is the models
import * as regiserService from '../services/register';
export default {
namespace: 'register',
state: {},
subscriptions: {
},
reducers: {
save(state, { payload: { data: message, code } }) {
return { ...state, message, code };
},
},
effects: {
*send({ payload }, { call, put }) {
const { data } = yield call(regiserService.sendAuthCode, { ...payload });
const message = data.message;
yield put({ type: 'save', payload: { data },});
},
},
};

handle conditions in the models solved the problem:
*send({ payload }, { call, put }) {
const { data } = yield call(regiserService.sendAuthCode, { ...payload });
if(data.code === 221){
Toast.fail("This phone has been registered", 1);
} else {
Toast.success("Send verify code successfully", 1);
}
yield put({ type: 'save', payload: { data }});
}

Related

Vuex: Wait for websocket response before dispatching action

So this is the scenario / premises:
In order to populate a chat queue in real time I need to open a connection to a websocket, send a message and then set the data to a websocket store. This store will basically manage all the websocket state.
Before populating the chat queue there's two parameters I need: a shiftId coming from one http API request and a connectionId coming from the websocket. Using those two parameters I finally can subscribe to a third http API and start receiving messages to populate the chat queue.
The problem is that due to the async behaviour of the websocket (or that's what I think, please feel to correct me if I'm wrong) I always get an empty "connectionId" when trying to make the put to that "subscription" API. I have tried with async/await and promises but nothing seems to work. I'm pretty new to async/await and websockets with Vuex so pretty sure I'm doing something wrong.
This is the user vuex module where I do all the login/token operations and dispatch a "updateEventsSubscription" action from the shift vuex module. In order for the "updateEventsSubscription" action to work I need to get the response from the "processWebsocket" action (to get the connectionId parameter) and from the "startShift" action (to get the shiftId parameter) coming from the shifts vuex module:
import UserService from '#/services/UserService.js'
import TokenService from '#/services/TokenService.js'
import router from '#/router'
export const namespaced = true
export const state = {
accessToken: '',
errorMessage: '',
errorState: false,
userEmail: localStorage.getItem('userEmail'),
userPassword: localStorage.getItem('userPassword'),
}
export const mutations = {
SET_TOKEN(state, accessToken) {
state.accessToken = accessToken
TokenService.saveToken(accessToken)
},
SET_USER(state, authUserJson) {
state.userEmail = authUserJson.email
state.userPassword = authUserJson.password
localStorage.setItem('userPassword', authUserJson.password)
localStorage.setItem('userEmail', authUserJson.email)
},
SET_ERROR(state, error) {
state.errorState = true
state.errorMessage = error.data.error_description
},
CLOSE_NOTIFICATION(state, newErrorState) {
state.errorState = newErrorState
},
}
export const actions = {
signIn({ commit, dispatch, rootState }, authUserJson) {
return UserService.authUser(authUserJson)
.then((result) => {
commit('SET_USER', authUserJson)
commit('SET_TOKEN', result.data.access_token)
dispatch('token/decodeToken', result.data.access_token, {
root: true,
})
dispatch(
'shifts/updateEventsSubscription',
rootState.token.agentId,
{
root: true,
}
)
router.push('/support')
})
.catch((error) => {
console.log(error)
if (error.response.status === 400) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
commit('SET_ERROR', error.response)
} else {
console.log(error.response)
}
})
},
signOut({ commit }) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
localStorage.removeItem('userPassword')
localStorage.removeItem('userEmail')
TokenService.removeToken()
router.push('/')
},
closeNotification({ commit }, newErrorState) {
commit('CLOSE_NOTIFICATION', newErrorState)
},
}
export const getters = {
getToken: (state) => {
return state.accessToken
},
errorState: (state) => {
return state.errorState
},
errorMessage: (state) => {
return state.errorMessage
},
isAuthenticated: (state) => {
return state.accessToken
},
userEmail: (state) => {
return state.userEmail
},
userPassword: (state) => {
return state.userPassword
},
}
This is websocket store: I pass the connectionId to the state in order to be able to use it in another vuex action to subscribe for new chats:
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChatInfo: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
if (wsData.connectionId) {
state.connectionId = wsData.connectionId
console.log(`Retrieving Connection ID ${state.connectionId}`)
} else {
console.log(`We got chats !!`)
state.messageType = wsData.type
state.incomingChatInfo = wsData.documents
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
And finally this is the shift store (where the problem is), as you can see I have a startShift action (everything works fine with it) and then the "updateEventsSubscription" where I'm trying to wait for the response from the "startShift" action and the "processWebsocket" action. Debugging the app I realize that everything works fine with the startShift action but the websocket action sends the response after the "updateEventsSubscription" needs it causing an error when I try to make a put to that API (because it needs the connectionId parameter coming from the state of the websocket).
import ShiftService from '#/services/ShiftService.js'
export const namespaced = true
export const state = {
connectionId: '',
shiftId: '',
agentShiftInfo: '{}',
}
export const actions = {
startShift({ commit }, agentId) {
return ShiftService.startShift(agentId)
.then((response) => {
if (response.status === 200) {
commit('START_SHIFT', response.data.aggregateId)
}
})
.catch((error) => {
console.log(error)
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
async updateEventsSubscription({ dispatch, commit, rootState }, agentId) {
await dispatch('startShift', agentId)
const shiftId = state.shiftId
await dispatch('websocket/processWebsocket', null, { root: true })
let agentShiftInfo = {
aggregateId: state.shiftId,
connectionId: rootState.websocket.connectionId,
}
console.log(agentShiftInfo)
return ShiftService.updateEventsSubscription(shiftId, agentShiftInfo)
.then((response) => {
commit('UPDATE_EVENTS_SUBSCRIPTION', response.data)
})
.catch((error) => {
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
}
export const mutations = {
START_SHIFT(state, shiftId) {
state.shiftId = shiftId
console.log(`Retrieving Shift ID: ${state.shiftId}`)
},
UPDATE_EVENTS_SUBSCRIPTION(state, agentShiftInfo) {
state.agentShiftInfo = agentShiftInfo
},
}
You should convert your WebSocket action into a promise that resolves when WebSocket is connected.:
export const actions = {
processWebsocket({ commit }) {
return new Promise(resolve=> {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
resolve();
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
});
},
}
So I realized that I have to resolve the promise on the this.ws.message instead. By doing that all my data is populated accordingly, there's still sync issues (I can't feed the websocket state at the moment because due to its async behaviour the state is not there yet when other components try to use it via: rootGetters.websocket.incomingChats for example) but I guess that's part of another question. Here's the final version of the module action:
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('wss://ws.rubiko.io')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
Anyways, thanks #Eldar you were in the right path.

Axios Put Request Not Updating Data | React Redux

I have been working on a project where I am trying to update my selected data but Axios didn't Update it even after giving a success msg.
User Response it returns from axios:-
completed: true
date: "2021-02-28"
mupp_path: "PATH 1 - LIVING YOUR WHY - Build/Apply/Inspire/Spread (BAIS) – Finding & Achieving Meaning and Purpose in work and life"
project_name: "Design and Test the Training Content for the i-Infinity 3 verticals "
selected: true
task_id: 14
task_name: "This is adding a new task to chekc full inbox functionality "
task_type: "THIS_WEEK"
Actions.js
export const taskTodayUnselect = (id) => async (dispatch) => {
try {
dispatch({ type: types.UNSELECTED_TASK_TODAY_REQUEST });
const { data } = await axios.put(
selectTaskForToday,
{
task_id: id,
selected: false,
},
{
headers: {
Authorization: `JWT ${token}`,
},
}
);
if (data) {
return dispatch({ type: types.UNSELECTED_TASK_TODAY_SUCCESS, payload: data });
}
} catch (error) {
return dispatch({ type: types.UNSELECTED_TASK_TODAY_FAILURE, payload: error });
}
};
thisweek.js
export default function ThisWeek() {
const unselectTaskTodayAPI = (id) => {
dispatch(taskTodayUnselect(id)).then((response) => {
let result = response.payload;
console.log(result);
if (result.success === 'true') {
notifySuccess(result.message);
fetchTaskData(categoryID);
}
});
};
const selectTask = (item) => {
if (item.selected) {
unselectTaskTodayAPI(item);
console.log('unselect');
} else {
selectTaskTodayAPI(item.task_id);
}
};
return (
<TaskDataComponent
item={item}
key={item.task_id}
label="This Week"
selectTask={selectTask}
/>
);
Don't Worry about the TaskDataComponent , it only handle the onClick function which invoke the selectedTask function

Issue with cancelling axios .then logic with interceptors and cancel token

I set up an axios response interceptor for my react app. It works fine for catching most errors but one thing i am having trouble with is if the response is a 401 aka the user is not authed, the interceptor sends the user back to the login page. Now this works but the logic inside the .then from the original request still runs. This causes a type error as in the .then logic i am setting a state with the response data. Here is my current attempt at implementing a axios cancel token that is not working. See the code below. What am i missing here? What is the best way to achieve this with out having to add If/Else logic to every axios request to check if "data" is there or is the response is a 401, 200 ...?
AxiosInterceptor.js
...
export default withRouter({
useSetupInterceptors: (history) => {
axios.interceptors.response.use(response => {
return response;
}, error => {
try {
if (error.response.status === 401) {
history.push("/login");
Swal.fire({
title: '401 - Authorization Failed',
text: '',
icon: 'warning',
showCancelButton: false,
confirmButtonText: 'Close',
})
throw new axios.Cancel('Operation canceled');
}
return Promise.reject(error);
} catch (error) {
console.log(error)
}
});
},
});
UserPage.js
...
function userPage() {
var [pageData, setPageData] = useState('');
var classes = useStyles();
useEffect(() => {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
const loadData = () => {
try {
axios.post('/api/getUserData', { cancelToken: source.token })
.catch(function (error) {
source.cancel();
})
.then(res => {
const data = res.data;
setPageData(data);
})
} catch (error) {
if (axios.isCancel(error)) {
console.log('Op Cancel')
} else {
throw error;
}
}
};
loadData();
return () => {
source.cancel();
};
}, []);
return (
...
);
}
...
The error i get:
Unhandled Rejection (TypeError): Cannot read property 'data' of undefined
PROGRESS UPDATE:
I added some logic to my back-end that if the login is successful,
i pass the expiration time of the JWT token back to my front end.
Then push that expiration epoch to my redux store.
On every request, in my 'AxiosInterceptor.js' file below, before returning a config back, i validate the exp value set in redux.
Now this works fine on initial login, but once the token has expired and you receive the popup from 'Swal.fire' and click 'return' it does two things:
calls logOut action and returns all values to initial state. (This works fine. I validated with redux-devtools-extension)
Now i can log back in. Everything starts to load fine but then i get the 'Swal.fire' dialog to return back to login page. When logging the user.exp and date.now to console i see some strange behavior(see comments):
// from redux-logger
action SET_EXP # 20:05:42.721
redux-logger.js:1 prev state {user: {…}, _persist: {…}}
redux-logger.js:1 action {type: "SET_EXP", payload: 1585267561036}
USEREXP 1585267561036 // this is the new EXP time set in redux, received from back end on login
AxiosInterceptors.js:17 Current Status = 1585267561036 false // first two axios calls on main page validate and indicate not expired
AxiosInterceptors.js:17 Current Status = 1585267561036 false
AxiosInterceptors.js:17 Current Status = 1585267495132 true // this is the value of the previos exp value that was set
AxiosInterceptors.js:17 Current Status = 1585267495132 true
AxiosInterceptors.js:17 Current Status = 1585267352424 true // this is the value that was set two login times ago
AxiosInterceptors.js:17 Current Status = 1585267352424 true
How is this possible? I verified with redux-devtools that once i am
returned back to the login page, it is indeed empty. It appears the value in > redux-store is being rolled back to old values? I am using chrome Version
74.0.3729.131 (Official Build) (64-bit). I have tried with incognito mode and clearing cache and cookies.
New AxiosInterceptor.js ...
export default withRouter({
useSetupInterceptors: (history) => {
let user = useSelector(state => state.user)
axios.interceptors.request.use(config => {
const { onLogo } = useLogout(history);
console.log("Current Status = ", user.exp, Date.now() > user.exp)
if (Date.now() > user.exp) {
Swal.fire({
title: '401 - Auth Failed',
text: '',
icon: 'warning',
showCancelButton: false,
confirmButtonText: 'Return',
}).then((result) => {
onLogo();
})
return {
...config,
cancelToken: new CancelToken((cancel) => cancel('Cancel')) // Add cancel token to config to cancel request if redux-store expire value is exceeded
};
} else {
return config;
}
}, error => { console.log(error)});
axios.interceptors.response.use(response => {
return response;
}, error => {
try {
if (axios.isCancel(error)) { // check if canceled
return new Promise(() => {}); // return new promise to stop axios from proceeding to the .then
}
if (error.response.status === 401) {
history.push("/login");
Swal.fire({
title: '401 - Auth Failed',
text: '',
icon: 'warning',
showCancelButton: false,
confirmButtonText: 'Close',
})
throw new axios.Cancel('Operation canceled');
}
return Promise.reject(error);
} catch (error) {
console.log(error)
}
});
},
});
function useLogo(history) {
const dispatch = useDispatch()
return {
onLogo() {
dispatch(allActs.userActs.logOut())
history.push("/login");
},
}
}
I tracked down the issue to the hook "useSelector" within react-redux. It seems this is some how returning cached data, after it already returned correct data. I am using version 7.2 at his time but i confirmed it also on v7.1. I have not tested on any other versions. I solved this by pulling the data from redux-persist Storage(localStorage) in the getExpire() function below. Not the most elegant solution but my application is now working as it should be.
export default withRouter({
useSetupInterceptors: (history) => {
const { onLogout } = useLogout(history);
const CancelToken = axios.CancelToken;
const { onExp } = useExp();
axios.interceptors.request.use((config) => {
const testexp = onExp();
if (testexp) {
Swal.fire({
title: '401 - Authorization Failed',
text: '',
icon: 'warning',
showCancelButton: false,
confirmButtonText: 'Return',
}).then((result) => {
onLogout();
})
return {
...config,
cancelToken: new CancelToken((cancel) => cancel('Cancel repeated request'))
};
} else {
return config;
}
}, error => { console.log(error) });
axios.interceptors.response.use(response => {
return response;
}, error => {
try {
if (axios.isCancel(error)) {
return new Promise(() => { });
}
return Promise.reject(error);
} catch (error) {
console.log(error)
}
});
},
});
function getExpire () {
var localStore = localStorage.getItem("persist:root")
if (localStore) {
let store = JSON.parse(localStore)
return JSON.parse(store.exp)
}
return 0
}
function useExp() {
// const currentExp = useSelector(state => state.exp)
return {
onExp() {
if (Date.now() > getExpire().exp) {
return true
} else { return false }
},
}
}
function useLogout(history) {
const dispatch = useDispatch()
return {
onLogout() {
dispatch(allActions.expAction.setLogout())
history.push("/login");
},
}
}

ReactJS - Proper way to get a result of dispatch

For async requests, I'm using redux-saga.
In my component, I call an action to recover the user password, it its working but I need a way to know, in my component, that the action I dispatched was successfully executed, like this:
success below is returning:
payload: {email: "test#mail.com"}
type: "#user/RecoverUserPasswordRequest"
__proto__: Object
My component:
async function onSubmit(data) {
const success = await dispatch(recoverUserPasswordRequestAction(data.email))
if (success) {
// do something
}
}
My actions.js
export function recoverUserPasswordRequest(email) {
return {
type: actions.RECOVER_USER_PASSWORD_REQUEST,
payload: { email },
}
}
export function recoverUserPasswordSuccess(email) {
return {
type: actions.RECOVER_USER_PASSWORD_SUCCESS,
payload: { email },
}
}
export function recoverUserPasswordFailure() {
return {
type: actions.RECOVER_USER_PASSWORD_FAILURE,
}
}
My sagas.js
export function* recoverUserPassword({ payload }) {
const { email } = payload
try {
const response = yield call(api.patch, 'user/forgot-password', {
email
})
// response here if success is only a code 204
console.log('response', response)
yield put(recoverUserPasswordSuccess(email))
} catch (err) {
toast.error('User doesnt exists');
yield put(recoverUserPasswordFailure())
}
}
export default all([
takeLatest(RECOVER_USER_PASSWORD_REQUEST, recoverUserPassword),
])
In my reducer.js I dont have nothing related to recover the user's password, like a RECOVER_USER_PASSWORD_SUCCESS because like I said, the api response from my saga is only a code 204 with no informations
You should treat this as a state change in your application.
Add a reducer that receives these actions RECOVER_USER_PASSWORD_SUCCESS or RECOVER_USER_PASSWORD_FAILURE, then updates the store with information about request status. For example:
const initialState = {
email: null,
status: null,
}
const recoverPasswordReducer = (state=initialState, action) => {
//...
if (action.type === actions.RECOVER_USER_PASSWORD_SUCCESS) {
return {...initialState, status: True }
}
if (action.type === actions.RECOVER_USER_PASSWORD_SUCCESS) {
return {...initialState, status: False }
}
return state;
}
You can later have status as one of the fields selected in mapStateToProps when connect the component that needs to know about the status of the operation to the store.
function mapStateToProps(state) {
return {
/* ... other fields needed from state */
status: state.status
}
}
export connect(mapStateToProps)(ComponentNeedsToKnow)

Axios 'Get' Function Not Calling

So, I am wanting to retrieve an updated list of contacts on once a new contact is added. Unfortunately, axios is only loading the get request on the 'beforeMount()' instance. When I try and call the function inside of an axios.post request when it's successful, the list of contacts is gone until I refresh the page again.
I'm running Laravel 5.7 and VueJs 2.5.22.
import axios from 'axios';
export default {
data() {
return {
companion: {
name: '',
desc: '',
primaryPhone: '',
secondaryPhone: '',
email: '',
address: '',
notes: '',
image: ''
},
characterAmount: 0
};
},
props: {
addCompanion: {
type: Boolean
}
},
methods: {
checkNotesLength(e) {
this.characterAmount =
document.getElementById('notes').value.length;
if (e.keyCode === 8) {
this.characterAmount--;
if (this.characterAmount < 0) {
this.characterAmount = 0;
}
} else {
this.characterAmount++;
if (this.characterAmount > 150) {
this.characterAmount = 150;
}
}
},
processFile(e) {
var input = e.target;
var reader = new FileReader();
reader.onload = (e) => {
this.companion.image = e.target.result;
}
reader.readAsDataURL(input.files[0]);
},
getCompanions() {
const url = window.location + 'companions';
axios.get(url)
.then((response) => {
this.companions = response.data;
})
.catch((error) => {
// handle error
console.log(error);
});
},
submitCompanion() {
const formData = {
name: this.companion.name,
desc: this.companion.desc,
primaryPhone: this.companion.primaryPhone,
secondaryPhone: this.companion.secondaryPhone,
email: this.companion.email,
address: this.companion.address,
notes: this.companion.notes,
image: this.companion.image
}
axios.post('/companion/create', formData)
.then(this.getCompanions())
.then((response) => {
this.addCompanion = !this.addCompanion;
//need to clear form and include messages, also need to add validation
})
.catch((error) => {
console.log(error);
});
}
}
}
The beforeMount() function is on my App.vue, which just calls the same getCompanions function as the above one you see.
The issue that I see in your code is that you are not passing the callback correctly. This code will execute the function getCompanions() immediately:
.then(this.getCompanions())
To pass it as a callback try something like this
.then(this.getCompanions.bind(this))
// OR
.then(() => this.getCompanions())
This is probably because your url structure is wrong.
const url = window.location + 'companions';
should be
const url = window.location + '/companions';

Categories