ReactJS - Proper way to get a result of dispatch - javascript

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)

Related

React app page routing issue post successful user login

I have a react app where I use the useContext and useReducer hooks for the login and storage. While the login part works, what I want achieve is to redirect user to a specific page post successful login. I am using react-router#6 and tried to use useNavigate() to navigate user to particular route though it doesn't seem to work.
const AuthService = async (dispatch) => {
const MSAL_CONFIG = {} // populate MSAL config for Microsoft Graph API for AD auth
const msalInstance = new msal.PublicClientApplication(MSAL_CONFIG);
try {
const loginResponse = await msalInstance.loginPopup(scopes);
var username = loginResponse.account.username;
var userid = username.slice(0, username.indexOf("#"));
const loginData = {
auth_token: loginResponse.idToken,
user: {
name: loginResponse.account.name,
id: userid,
email: username,
},
};
const sessionData = {
user_id: userid,
id_token: loginResponse.idToken,
access_token: loginResponse.accessToken,
}
sessionStorage.setItem("currentUser", JSON.stringify(loginData));
dispatch({ type: "LOGIN_SUCCESS", payload: loginData });
return { loginData: loginData, error: null };
// dispatch({ type: 'LOGIN_SUCCESS', payload: loginData });
//sessionStorage.setItem('currentUser', JSON.stringify(data));
} catch (err) {
console.log("+++ Login error : ", err);
dispatch({ type: "LOGIN_ERROR", error: err });
return { loginData: null, error: err };
}
};
In my header.jsx, I have below code to handle the login button. It makes a call to the above AuthService. The code post AuthService() call, i.e. the if block, doesn't take effect, so user never gets redirected to the dashboard page.
const handleLogin = async () => {
await AuthService(dispatch)
console.log("userDetails.token : " + userDetails.token)
if (Boolean(userDetails.token)) {
navigate("/dashboard");
}
};
If I'm correct in understanding that this AuthService function eventually resolves and that the dispatched LOGIN_SUCCESS action updates the userDetails variable that is selected from the auth context state, then I think you have all that you need and are close to a working solution. The issue is that the userDetails value from the render cycle the handleLogin is called in is closed over in callback scope, it will never be a different value. If the userDetails.token value is falsey when handleLogin is called, it will remain falsey in the entire callback scope.
The AuthService function appears to return the same loginData object that is passed in the dispatched LOGIN_SUCCESS action to the store. handleLogin should await this value and conditionally navigate.
const AuthService = async (dispatch) => {
...
try {
const { account, idToken } = await msalInstance.loginPopup(scopes);
const { name, username } = account;
const userid = username.slice(0, username.indexOf("#"));
const loginData = {
auth_token: idToken,
user: {
name,
id: userid,
email: username,
},
};
...
sessionStorage.setItem("currentUser", JSON.stringify(loginData));
dispatch({ type: "LOGIN_SUCCESS", payload: loginData });
return { loginData, error: null }; // <-- return value
} catch (error) {
dispatch({ type: "LOGIN_ERROR", error });
return { loginData: null, error }; // <-- return value
}
};
const handleLogin = async () => {
const { loginData } = await AuthService(dispatch);
if (loginData && loginData.auth_token) { // or loginData?.auth_token
navigate("/dashboard", { replace: true });
}
};

Not able to get the id of the generated firebase document

I'm trying to get the id of the generated firebase document, and I'm using addDoc to create a new doc.
I'm generating a new document on button click and that button calls the initializeCodeEditor function.
Anyone please help me with this!
Button Code:
import { useNavigate } from "react-router-dom"
import { useAuthContext } from "../../hooks/useAuthContext"
import { useFirestore } from "../../hooks/useFirestore"
import Button from "./Button"
const StartCodingButton = ({ document, setIsOpen }) => {
const { user } = useAuthContext()
const { addDocument, response } = useFirestore("solutions")
const navigate = useNavigate()
const initializeCodeEditor = async () => {
await addDocument({
...document,
author: user.name,
userID: user.uid,
})
if (!response.error) {
console.log(response.document) // null
const id = response?.document?.id; // undefined
navigate(`/solution/${id}`, { state: true })
}
}
return (
<Button
className="font-medium"
variant="primary"
size="medium"
onClick={initializeCodeEditor}
loading={response.isPending}
>
Start coding online
</Button>
)
}
export default StartCodingButton
addDocument code
import { useReducer } from "react"
import {
addDoc,
collection,
doc,
Timestamp,
} from "firebase/firestore"
import { db } from "../firebase/config"
import { firestoreReducer } from "../reducers/firestoreReducer"
const initialState = {
document: null,
isPending: false,
error: null,
success: null,
}
export const useFirestore = (c) => {
const [response, dispatch] = useReducer(firestoreReducer, initialState)
// add a document
const addDocument = async (doc) => {
dispatch({ type: "IS_PENDING" })
try {
const createdAt = Timestamp.now()
const addedDocument = await addDoc(collection(db, c), {
...doc,
createdAt,
})
dispatch({ type: "ADDED_DOCUMENT", payload: addedDocument })
} catch (error) {
dispatch({ type: "ERROR", payload: error.message })
}
}
return {
addDocument,
response,
}
}
firestoreReducer
export const firestoreReducer = (state, action) => {
switch (action.type) {
case "IS_PENDING":
return { isPending: true, document: null, success: false, error: null }
case "ADDED_DOCUMENT":
return { isPending: false, document: action.payload, success: true, error: null }
}
throw Error("Unknown action: " + action.type)
}
I have recreated this issue and found out this is happening because the response object in the useFirestore hook is not being updated until the next render cycle.
In order to get the updated response object, you can use the useEffect hook to trigger an update to the component whenever the response object changes.
So I recommend you to call initializeCodeEditor and make your app wait until response object change I used useEffect here
const initializeCodeEditor = async () => {
await addDocument({
author: user.name,
userID: user.uid,
})
//skip following if block it's just for understanding
if (!response.error) {
console.log(response.document) // will obviously be null here as at first it is set null
const id = response?.document?.id; // will obviously be undefined
navigate(`/solution/${id}`, { state: true })
}
}
useEffect(() => {
if (!response.error) {
setId(response?.document?.id);
console.log("From App.js useEffect: " + response?.document?.id); // getting the document id here too
}
}, [response])
//and in firestoreReducer
case "ADDED_DOCUMENT":{
console.log("from Reducer: " + action.payload.id); //getting the document id here
return { isPending: false, document: action.payload, success: true, error: null }
}
OR you can use callback also without introducing useEffect like this:
const initializeCodeEditor = async () => {
await addDocument({
author: user.name,
userID: user.uid,
}, (response) => {
console.log("From App: " + response?.document?.id); //Will run as callback
if (!response.error) {
setId(response?.document?.id);
}
})
}
This way, the callback function will be called after the addDocument function has completed and the response object will have the updated document id.

Redux - Returns call function informations to update props / state

I am an intermediate developer on React and I would like to have advice on the best practice to update props with SET_STATE in reducer.
This is my file nomenclature :
redux/
├── events/
│ ├── reducer.js
│ ├── actions.js
│ ├── sagas.js
├── services/
│ ├── events.js
When I dispatch an action in sagas.js, the action function calls another function in events.js that calls the API.
I would like to update the props top revent reload of my web page afetr each modification.
Now when I call a function in events.js with yield call(...) [in sagas.js], I always receive undifined while I return the API response data.
I would like to receive this information to call my SET_STATE action in my reducer to update it without relaod
This is my files :
reducer.js
import actions from './actions'
const initialState = {
uniqueEvent: {},
logistic: []
}
export default function eventsReducer(state = initialState, action) {
switch (action.type) {
case actions.SET_STATE:
{
return { ...state, ...action.payload }
}
default:
return state
}
}
actions.js
const actions = {
SET_STATE: 'events/SET_STATE',
UPDATE_EVENT: 'events/UPDATE_EVENT',
ADD_EVENT_LOGISTIC: 'events/ADD_EVENT_LOGISTIC',
}
export default actions
sagas.js
import { all, takeEvery, put, call } from 'redux-saga/effects'
import { pdateEvent, addEventLogistic } from 'services/events'
import actions from './actions'
export function* UPDATE_EVENT({ payload: { event } }) {
console.log(JSON.stringify(`UPDATE_EVENT`))
const upEvent = yield call(updateEvent, event)
if(upEvent){
yield put({
type: 'events/SET_STATE',
payload: {
uniqueEvent: upEvent,
},
})
} else {
console.log(JSON.stringify('REDUX UPDATE_EVENT NOT DONE'))
}
}
export function* ADD_EVENT_LOGISTIC({ payload: { activist, information, eventId } }) {
console.log(JSON.stringify(`ADD_EVENT_LOGISTIC`))
const logisticToAdd = {user: activist, description: information, event: eventId}
const eventLogistic = yield call(addEventLogistic, logisticToAdd)
if (eventLogistic) {
yield put({
type: 'events/SET_STATE',
payload: {
logistic: eventLogistic,
},
})
} else {
console.log(JSON.stringify('REDUX EVENT LOGISTIC NOT DONE'))
}
}
export default function* rootSaga() {
yield all([
takeEvery(actions.UPDATE_EVENT, UPDATE_EVENT),
takeEvery(actions.ADD_EVENT_LOGISTIC,ADD_EVENT_LOGISTIC),
])
}
And my events.js (api call):
import { notification } from 'antd'
const axios = require('axios')
export function updateEvent(value) {
axios.put(`http://api.xxxx.xx/xx/xxxx/events/event/${value.id}/`, value, {
headers: {
Authorization: `Token ${localStorage.getItem('auth_token')}`,
},
})
.then(response => {
return response.data
})
.catch(function(error) {
notification.error({
message: error.code,
description: error.message,
})
})
}
export function addEventLogistic(value) {
axios.post(`http://api.xxxx.xx/xx/xxxx/events/logistic/`, value, {
headers: {
Authorization: `Token ${localStorage.getItem('auth_token')}`,
},
})
.then(response => {
return response.data
})
.catch(function(error) {
notification.error({
message: error.code,
description: error.message,
})
})
}
export default async function defaulFunction() {
return true
}
I would like the addEventLogstic() function in events.js return the JSON from the request (I did some tests and I get it well) but in sagas.js the variable declared with yield call(...) does not get any value (whereas I would like the JSON of the API).
Do you have any thoughts?
Thank you very much for your precious help
I suspect it's your updateEvent and addEventLogistic function not returning anything. Add a return statement so they do:
export function updateEvent(value) {
//added return statement here
return axios.put(`http://api.xxxx.xx/xx/xxxx/events/event/${value.id}/`, value, {
headers: {
Authorization: `Token ${localStorage.getItem('auth_token')}`,
},
})
.then(response => {
return response.data
})
.catch(function(error) {
notification.error({
message: error.code,
description: error.message,
})
})
}

multiple api call actions in the redux thunk

I am using redux-thunk . Here, I have one login action. On that action I am calling an API which will give me some token, that I have to store in the state. Then immediately, after success of this action, I have to make another API request which will have this token in the header and will fetch more data. Based on this, I would like to redirect the user.
Login Action
import { generateToken } from '../APIs/login';
import HttpStatus from 'http-status-codes';
import { LOGIN_FAILED, LOGIN_SUCCESS } from '../constants/AppConstants';
import { fetchUserJd } from './GetUserJd';
import history from '../history';
export function fetchToken(bodyjson) {
return (dispatch) => {
getLoginDetails(dispatch, bodyjson);
}
}
export function getLoginDetails(dispatch, bodyjson) {
generateToken(bodyjson)
.then((response) => {
if (response.status === 200)
dispatch(sendToken(response.payload))
else
dispatch(redirectUser(response.status));
})
}
export function sendToken(data) {
return {
type: LOGIN_SUCCESS,
data: data,
}
}
export function redirectUser(data) {
return {
type: LOGIN_FAILED,
data: data,
}
}
2nd Action
import { FETCHING_JOBDESCRIPTION_SUCCESS, FETCHING_DATA_FAILED,FETCHING_JOBS } from '../constants/AppConstants';
import { getUserJobs } from '../APIs/GetUserJd';
import history from '../history';
export function fetchUserJd(token) {
console.log(token);
return (dispatch) => {
dispatch(fetchingJobDescription());
}
};
export function getUserJd(dispatch, token) {
getUserJobs(token)
.then((response) => {
if (response.status === 200)
dispatch(sendUserJd(response.payload))
else
dispatch(fetchFailure(response.status));
})
}
export function fetchFailure(data) {
return {
type: FETCHING_DATA_FAILED,
data: data,
}
}
export function sendUserJd(data) {
return {
type: FETCHING_JOBDESCRIPTION_SUCCESS,
data: data,
}
}
export function fetchingJobDescription() {
return {
type: FETCHING_JOBS
}
}
Calling this from
handleClick(event) {
event.preventDefault();
var bodyJson = {
"username": this.state.UserName,
"password": this.state.password
}
this.props.fetchToken(bodyJson);
}
How can I call that second action immediately after the success of the first request. ?
Tried way ->
componentWillReceiveProps(newProps) {
console.log(newProps.token);
if(newProps.token) {
this.props.fetchUserJd(newProps.token);
}
}
export function sendUserJd(data) {
if (data.data.length > 0) {
history.push('/userJobs');
} else {
history.push('/createJob');
}
return {
type: FETCHING_JOBDESCRIPTION_SUCCESS,
data: data,
}
}
You can do without setting it to redux state. You need to return your success action call to get the token in component itself using promise .then and then call this.props.sendToken(token); which will actually set the data in state and follows your existing flow.
handleClick(event) {
event.preventDefault();
var bodyJson = {
"username": this.state.UserName,
"password": this.state.password
}
this.props.getLoginDetails(bodyJson).then((token) => {
this.props.sendToken(token);
});
}
And in actions
const GET_LOGIN_DETAILS_SUCCESS = 'GET_LOGIN_DETAILS_SUCCESS';
export function getLoginDetailsSuccess(data) {
return {
type: GET_LOGIN_DETAILS_SUCCESS,
data: data,
}
}
export function getLoginDetails(bodyjson) {
generateToken(bodyjson)
.then((response) => {
if (response.status === 200)
return dispatch(getLoginDetailsSuccess(response.payload))
else
dispatch(redirectUser(response.status));
})
}
Let me know if you have any questions or if you feel difficult to understand

How to handle conditions after asynchronous request

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 }});
}

Categories