Transition to a route on successful async Redux action - reactjs

When I am deleting the category with an API call, Redux doesn't wait for all actions to complete. Sample action below:
export const deleteCategory=(id)=>{
return dispatch=>{
const message='Category Deleted Successfully!'
dispatch(deleteCategoryStart())
axios.delete(`http://127.0.0.1:8000/admindashboard/categories/${id}/`)
.then(res=>{
dispatch(deleteCategorySuccess(message))
}).catch(err=>{
dispatch(deleteCategoryFailed(err))
})
}
}
My code to handle the delete:
handleDelete= (event) =>{
event.preventDefault();
this.props.deleteCategory(this.props.match.params.categoryID);
this.props.history.push('/categories')
}
I am trying to delete the category and redirect the user to the "all categories" page. Instead, it's redirecting the user before all action dispatches are completed. The new categories page/component calls additional actions, getting all categories.
Action sequence:

JavaScript explicitly won't wait for your external process (API call) to complete. You can use async/await or change your code similar to below
export const deleteCategory = (id) => {
return dispatch => {
dispatch(deleteCategoryStart());
return axios.delete(`http://127.0.0.1:8000/admindashboard/categories/${id}/`);
}
}
handleDelete = (event) => {
event.preventDefault();
this.props.deleteCategory(this.props.match.params.categoryID).then(res => {
const message='Category Deleted Successfully!';
this.props.dispatch(deleteCategorySuccess(message));
this.props.history.push('/categories'); // Push to the page here
}).catch(err => {
this.props.dispatch(deleteCategoryFailed(err));
});
}

Related

How to check if redux actionReducer axios request is successful or not

I'm creating a page login with OTP, so first I sent request for login. If it's successful then my model would open for entering the otp.
But in my case model will open always because im unable to figure out how check if my request is successfult or not
I can manage state but I don't want to manage state for this simple task. I just want that the request in action reducer is successful or not. This will surely solves my problem easily.
const handleSubmit = async (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
let email= data.get('email');
let password= data.get('password');
await props.otpRequest(email,password);
handleOTPModelOpen();
};
Action Reducer
export const otpRequest = (email,password) => {
return (dispatch) => {
const url = process.env.REACT_APP_DEV_BACKEND
dispatch(getOTPRequest())
axios
.post(`${url}/api/get_otp`,{
email: email,
password: password
})
.then(response => {
dispatch(getOTPSuccess())
dispatch(showAlert(handleAxiosError(response),"success"));
})
.catch(error => {
dispatch(showAlert(handleAxiosError(error),"error"));
dispatch(getOTPFailure())
throw OTPRequestFailed()
})
}
}
and using mapDispatchToProps
const mapDispatchToProps = dispatch => {
return {
fetchToken: (email,otp) => dispatch(fetchToken(email,otp)),
otpRequest: (email,password) => dispatch(otpRequest(email,password))
}
}
I just want to check if otpRequest is successful or not. Like that we checkedd in axios request in action reducer.
All request are successful no error is coming
One nice way of handling this for most libraries is a try / catch block.
You could do this:
try {
await props.otpRequest(email,password);
handleOTPModelOpen();
} catch (e) {
// handle error here
console.log(e);
handleOTPFailed();
}
EDIT 1
I don't see a selector or use of mapStateToProps callback. You use mapDispatchToProps which is great for the actions, however, in order to access the resulting state, you must also add mapStateToProps. Here's an example from one of my apps:
const mapStateToProps = (state) => {
return {
success: state.data.success;
}
}
Think of state as a whole pie. A Redux selector allows you to take a slice of that pie and return it to the React component props for use, instead of the entire state (response from the API dispatch).

React and Redux toolkit - reject after promise

I'm working on a React Native app. I have a signup screen which has a button, onclick:
const handleClick = (country: string, number: string): void => {
dispatch(registerUser({ country, number }))
.then(function (response) {
console.log("here", response);
navigation.navigate(AuthRoutes.Confirm);
})
.catch(function (e) {
console.log('rejected', e);
});
};
The registerUser function:
export const registerUser = createAsyncThunk(
'user/register',
async ({ country, number }: loginDataType, { rejectWithValue }) => {
try {
const response = await bdzApi.post('/register', { country, number });
return response.data;
} catch (err) {
console.log(err);
return rejectWithValue(err.message);
}
},
);
I have one of my extraReducers that is indeed called, proving that it's effectively rejected.
.addCase(registerUser.rejected, (state, {meta,payload,error }) => {
state.loginState = 'denied';
console.log(`nope : ${JSON.stringify(payload)}`);
})
But the signup component gets processed normally, logging "here" and navigating to the Confirm screen. Why is that?
A thunk created with createAsyncThunk will always resolve but if you want to catch it in the function that dispatches the thunk you have to use unwrapResults.
The thunks generated by createAsyncThunk will always return a resolved promise with either the fulfilled action object or rejected action object inside, as appropriate.
The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an unwrapResult function that can be used to extract the payload of a fulfilled action or to throw either the error or, if available, payload created by rejectWithValue from a rejected action:
import { unwrapResult } from '#reduxjs/toolkit'
// in the component
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then(originalPromiseResult => {})
.catch(rejectedValueOrSerializedError => {})
}

How do I load firebase data into react-redux asynchronously?

I am currently trying to load my product data into redux, but so far I cant seem to pass the product information returned from firestore into the reducer.
Index.js -> load first 10 products from firestore soon after store was created.
store.dispatch(getAllProducts)
action/index.js
import shop from '../api/shop'
const receiveProducts = products => ({
type: types.RECEIVE_PRODUCTS
products
})
const getAllProducts = () => dispatch => {
shop.getProducts(products => {
dispatch(receiveProducts)
})
}
shop.js
import fetchProducts from './firebase/fetchProducts'
export default {
getProducts: (cb) => cb(fetchProducts())
}
fetchProducts.js
const fetchProducts = async() => {
const ProductList = await firebase_product.firestore()
.collection('store_products').limit(10)
ProductList.get().then((querySnapshot) => {
const tempDoc = querySnapshot.docs.map((doc) => {
return { id: doc.id, ...doc.data() }
})
}).catch(function (error) {
console.log('Error getting Documents: ', error)
})
}
In product reducers
const byId = (state={}, action) => {
case RECEIVE_PRODUCTS:
console.log(action); <- this should be products, but it is now promise due to aysnc function return?
}
I can get the documents with no issues (tempDocs gets the first 10 documents without any issue.) but I am not able to pass the data back into my redux. If I were creating normal react app, I would add a loading state when retrieving the documents from firestore, do I need to do something similar in redux as well ?
Sorry if the code seems messy at the moment.
fetchProducts is an async function so you need to wait for its result before calling dispatch. There are a few ways you could do this, you could give fetchProducts access to dispatch via a hook or passing dispatch to fetchProducts directly.
I don't quite understand the purpose of shop.js but you also could await fetchProducts and then pass the result of that into dispatch.
A generalized routine I use to accomplish exactly this:
const ListenGenerator = (sliceName, tableName, filterArray) => {
return () => {
//returns a listener function
try {
const unsubscribe = ListenCollectionGroupQuery(
tableName,
filterArray,
(listenResults) => {
store.dispatch(
genericReduxAction(sliceName, tableName, listenResults)
);
},
(err) => {
console.log(
err + ` ListenGenerator listener ${sliceName} ${tableName} err`
);
}
);
//The unsubscribe function to be returned includes clearing
// Redux entry
const unsubscriber = () => {
//effectively a closure
unsubscribe();
store.dispatch(genericReduxAction(sliceName, tableName, null));
};
return unsubscriber;
} catch (err) {
console.log(
`failed:ListenGenerator ${sliceName} ${tableName} err: ${err}`
);
}
};
};
The ListenCollectionGroupQuery does what it sounds like; it takes a tableName, an array of filter/.where() conditions, and data/err callbacks.
The genericReduxAction pretty much just concatenates the sliceName and TableName to create an action type (my reducers de-construct action types similarly). The point is you can put the dispatch into the datacallback.
Beyond this, you simply treat Redux as Redux - subscribe, get, etc just as if the data were completely local.

Wait For Dispatch Action to be completed outside the component

I have a screen that user will choose a type of Quiz from, then quiz questions should be generated, currentGameInformation should be updated in the store and then new screen should be visible.
Since dispatch actions are async, sometimes the currentGameInformation is not updated, so my app crashes when it gets to next page. I want it to wait until it goes to next page so the information is available.
On Press of a button, a function is called in my component called startTheGame()
//inside the screen component
startTheGame = async (id) => {
let navigation = this.props.navigation;
await StartTheGame(MASTER_TIME_PERIOD, {time_period_id: id}).then(function(){
console.log("Navigating");
navigation.replace('Quiz');
});
};
//This function is located outside the component,
//It is a library that handles all the Quiz functionalities
export async function StartTheGame(type, details) {
let state = store.getState();
let username = state.currentUser.username;
if(username === undefined){
//AWS gets the current user working fine and waiting to be completed
let user = await GetCurrentUserAWS();
username = user.username;
}
//set game status to loading
let currentGameInfo = {};
let currentDayPoints = await GetCurrentDayPointsForUserDB(username);
//Redux Thunk function (is sent, but not waiting to get done)
SetCurrentDayPoints(currentDayPoints);
//redux thunk function (is set but not waiting for it to be done)
SetGameStatus(SET_GAME_START_LOADING, QUIZ_GAME_START_STATUS_LOADING);
//at this point, current day points are either updated/not and same with game status
let questions = await GenerateQuestions(type, details).catch(err => {
SetGameStatus(SET_GAME_START_ERROR, QUIZ_GAME_START_STATUS_ERROR); //same not waiting to be completed
});
currentGameInfo = {
questions: questions,
points: 0,
questionIndexesAnsweredCorrectly: [],
questionIndexesAnsweredIncorrectly: [],
shouldRestartBeEnabled: false,
currIndex:0,
questionsAnsweredInRow:0,
gameType:type
};
SetGameStatusSuccess(currentGameInfo); //same not waiting
return currentGameInfo; }
My goal is to return only after SetGameStatusSuccess has been completed
export function SetGameStatusSuccess(currentGameInfo){
return (dispatch, getState) => {
dispatch({type: SET_GAME_START_SUCCESS, payload:{
gameStatus:QUIZ_GAME_START_STATUS_STARTED,
currentGameInformation:currentGameInfo
}});
}; }
export function SetGameStatus(gameStatus, quizStatus){
return (dispatch, getState) => {
dispatch({type: gameStatus, payload:{gameStatus:quizStatus}});
};}
I am wondering if there is a way to do this without the need of mapDispatchToProps function ?
You need to await your SetGameStatus function call. Since your StartTheGame function is marked as async, all you need to do is:
let currentDayPoints = await GetCurrentDayPointsForUserDB(username);
SetCurrentDayPoints(currentDayPoints);
//add 'await' here
await SetGameStatus(SET_GAME_START_LOADING, QUIZ_GAME_START_STATUS_LOADING);
and same for here:
let questions = await GenerateQuestions(type, details).catch(asybc (err) => {
await SetGameStatus(SET_GAME_START_ERROR, QUIZ_GAME_START_STATUS_ERROR);
});

Unsubscribe to firestore(...).onSnapshot() does not work when dispatch() is involved

Main goal: unsubscribe correctly all firestore-listeners before logging out the user, preventing leaks.
Libraries involved: react, react-native, redux, redux-thunk and react-native-firebase.
Problem: Unsubscribe to firestore(...).onSnapshot() does not work when dispatch() is involved.
I fetch data with onSnapshot and returns the unsubscribe function to the caller component which I call on user logout. Strangely, UNSUBSCRIBE only works when no dispath is made...
I have a component (component.js) that is connected to redux store and fetch constantly some user data like this:
componentDidMount() {
this.unsubscribe = this.props.userFetch(); // userFetch is an action creator in actions.js
}
In actions.js
import firestore from '#react-native-firebase/firestore';
import auth from '#react-native-firebase/auth';
export const userFetch = () => {
return dispatch => {
const unsubscribe = firestore()
.doc(`users/${auth().currentUser.uid}`)
.onSnapshot({
error: e => console.warn('ERROR IN FETCH: ', e),
next: SnapshotUser => {
console.log('User: ', SnapshotUser.data());
// Will dispatch action below
},
});
return unsubscribe;
};
};
Note that there is no DISPATCH for the moment in the previous action creator.
If I call unsubscribe in component.js, the firestore onSnapshot listener gets unsubscribed correctly, like this:
onLogoutPressed = () => {
this.unsubscribe(); // <-- HERE it works (for the moment...)
auth()
.signOut()
.then(() => {
console.log('user has been signout');
})
.catch(error => {
console.log('Error: ',error);
});
};
Now if I want to send my fetched data to the redux store with a dispatch, I add the dispatch like this in actions.js
export const userFetch = () => {
return dispatch => {
const unsubscribe = firestore()
.doc(`users/${auth().currentUser.uid}`)
.onSnapshot({
error: e => console.warn('ERROR IN FETCH: ', e),
next: SnapshotUser => {
console.log('User: ', SnapshotUser.data());
// Will dispatch action below
dispatch({ // <--------------------------------- HERE
type: 'USER_FETCH_SUCCESS',
payload: SnapshotUser.data(),
});
},
});
return unsubscribe;
};
};
But then suddenly in my component.js, the this.unsubscribe doesn't work anymore on logout.
I've found that guy doing the same but works for him on React: here.
The solution provided by this other guy is basically the same too.
It looks like the firestore-onsnapshot-listener is wrapped in some dispatch call due to redux-thunk and I cant understand how it behaves now.
Does someone has any solution?
Ok solved it with the help of #ioss on Reactiflux.
The componentDidMount was mounted twice for some weird reasons, creating multiple listeners thus unmounting one was not enough.
Solved it by adding another run on unsubscribe() in componentWillUnmount().

Resources