Redux Saga authentication with router 6 - reactjs

I am porting an application from Router v2 to Router v6.
I am attempting to get authentication to work.
This bit of code gets hit when the user hits login
sendLoginRequest(event) {
// Prevent the page from unloading from the form submit
event.preventDefault();
// Don't do anything if the form is not valid
if (!this.isFormValid()) {
return;
}
// Make the request to the server to login
this.props.login({
emailAddress: this.state.emailAddress,
password: this.state.password,
onLoginSuccess: () => {
ReactGA.event({
category: 'Login',
action: 'success',
label: 'From Login Page'
});
// Login was successful, send the user to the main page
this.props.navigate('/');
},
onLoginFailure: (error) => {
ReactGA.event({
category: 'Login',
action: 'failure',
label: 'From Login Page'
});
// Set an error message depending on the type of error
this.setErrorMessage(error);
}
});
}
mapDisptachToProps
static mapDispatchToProps(dispatch) {
return {
login: (options) => dispatch({type: LOGIN, ...options}),
getSettings: () => dispatch({type: GET_SETTINGS})
};
}
It gets to this.props.login, and goes through the reducers, as I would expect.
However, none of the reducers I have match the above layout with the exception of the below, which exists in the sagas\authenticator.js
function* login() {
while (true) {
const {emailAddress, password, onLoginSuccess, onLoginFailure} = yield take(actions.LOGIN);
try {
// Make the request to login only if the user is not currently logged in
const isLoggedIn = yield select(_isLoggedIn);
if (!isLoggedIn) {
yield call(actions.login, emailAddress, password);
yield put({type: actions.GET_PROFILE});
// Call the success callback if one is provided
if (onLoginSuccess) {
onLoginSuccess();
}
}
} catch (error) {
// Call the failure callback if one is provided
if (onLoginFailure) {
onLoginFailure(error);
}
}
}
}
This contains some sort of reference to the above code blocks calls, this also however never gets hit.
What am I missing here? I get no errors of any kind, just a lack of functionality.

What I needed to do in this scenario was ensure my fork strategy for Redux was changed from
export default function* root() {
yield [
fork(login),
fork(logout),
fork(isLoggedIn),
fork(getProfile),
fork(forgotPassword),
fork(resetPassword)
];
}
to
export default function* root() {
yield fork(login);
yield fork(logout);
yield fork(isLoggedIn);
yield fork(getProfile);
yield fork(forgotPassword);
yield fork(resetPassword);
}
and now the proper bit of code is being called and the API is being hit.
However I am unsure if this has other implications I'm perhaps unaware of, and will update this post when I learn more.

Related

await of generator completing in redux-saga

I have code in component,
I need to get updated authorizedError value in function, but i get old value authorized error
// login component
const authorizedError = useSelector((state: RootState) => state.user.authorizedError);
const onSignInPress = useCallback(async () => {
await dispatch(userActions.postLoginUser({username: email, password}));
if (authorizedError) {
setNotificationErrors(['Wrong login or password'])
showNotification();
}
}, [authorizedError, validate, email, password]);
// postLoginUserSaga.js
export default function* postLoginUserSaga({
payload,
}: PayloadAction<UserCredentialsPayload>) {
try {
yield put(setSignInError(false));
const {
data: {
payload: { access_token },
status,
},
} = yield transport.post(URLS.postLoginUserURL, payload);
if (status !== "Ok") {
throw new Error(status);
}
yield setItemAsync(ACCESS_TOKEN_KEY, access_token);
yield put(setSignIn(true));
} catch (error) {
console.error("User login failed", error);
yield put(setSignInError(true));
}
}
// sagaRoot file
export default function* userRootSaga() {
yield all([
checkAuthSaga(),
takeEvery(actions.postLoginUser, postLoginUserSaga),
takeEvery(actions.postRegistrationUser, postRegistrationUserSaga),
takeEvery(actions.getProfileData, getProfileDataSaga),
]);
}
Redux actions don't return a promise, you can't use them like this.
If you want to use the promise API you can use the redux-thunk middleware which supports it.
If you want to use sagas you can add a callback action property instead.
// in component callback
dispatch(userActions.postLoginUser({username: email, password, callback: (authorizedError) => {
if (authorizedError) {
setNotificationErrors(['Wrong login or password'])
showNotification();
}
}));
// in saga
try {
...
action.callback();
} catch (err) {
action.callback(err);
}
Although that has its own issues.
Usually you communicate from sagas back to components by changing the redux state, so you can e.g. have a state for redux error, and based on that field show the error message or show different component if the login was succesful.

How to display a message on screen depending on the response from a saga handler

Contact Saga handler
export function* handlePostContactUser(action) {
try {
yield call(axios.post, '*endpoint*', action.data);
} catch (error) {
throw error;
}
};
Front-end form handleSubmit function:
let handleContactFormSubmit = () => {
let name = input.name;
let email = input.email;
let message = input.message;
dispatch({ type: 'POST_CONTACT_USER', data: {name, email, message, date}});
}
RootSaga
export function* watcherSaga() {
yield all([
takeEvery("POST_CONTACT_USER", handlePostContactUser)
]);
};
Based on this code, how could I display a message on the front end after the form submits, based on if it was successful or not? If it was, then just redirect/refresh the page, if not, display an error on the screen for the user to see
Since sagas are basically generators, what you need to do is fire an action after the yield call(axios). Change the store this way and get the result in your component.
In order to handle an error, put an action in the catch block doing the same thing.
export function* handlePostContactUser(action) {
try {
yield call(axios.post, '*endpoint*', action.data);
yield put('MY_SUCCESS_ACTION')
} catch (error) {
yield put('MY_ERROR_ACTION')
}
};
Update the store like this and then get the store properties that you need in the component.

React, Firebase - signInWithEmailAndPassword - cannot use the user variable outside .then() block

I am trying to set up a react-app which uses firebase authentication only with email and password.
When you have a look at googles documentation for signing in with email and password, you find the following code:
firebase.auth().createUserWithEmailAndPassword(email, password)
.then((userCredential) => {
// Signed in
var user = userCredential.user;
// ...
})
.catch((error) => {
var errorCode = error.code;
var errorMessage = error.message;
// ..
});
In my application, I get auth and the submitted email / password via action.formState.values.email / action.formState.values.password.
initialState is the default user object, which I am then trying to modify and return for the function sessionReducer.
I have implemented it the following way:
import * as actionTypes from 'actions';
import { auth } from '../firebase';
const initialState = {
loggedIn: false,
user: {
first_name: 'First Name',
last_name: 'Second Name',
email: 'email#email.com',
avatar: '/images/avatars/avatar_11.png',
bio: 'Titel/Bio',
role: 'ADMIN'
}
};
const sessionReducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.SESSION_LOGIN: {
auth.signInWithEmailAndPassword(action.formState.values.email, action.formState.values.password)
.then((userCredential) => {
// Signed in
var user = userCredential.user;
return user;
})
.catch((error) => {
// Print error message
});
// ------ Cannot access user object from firebase here ------ //
return {
loggedIn: true,
user: {
...initialState.user,
email: user.email // <- Here I need the Email out of the user object from firebase
}
};
}
}
};
export default sessionReducer;
When I print the user object from firebase directly in the .then() block, I get everything I need but as soon as I want to use this user variable outside, after the the then block block, I don't have access to it.
I think the problem is, that the return statement runs too early... The firebase-call has not yet finished but the return statement already tries to access the user variable from firebase.
If you need any more information, just ask as I am not sure how much I have to provide...
Thanks for your help!
As Doug mentioned in the comment you can't use async code in a pure redux. To make your code work try to use something like redux-thunk or redux-saga. This example should explain a little bit how it works:
function makeASandwich(forPerson, secretSauce) {
return {
type: 'MAKE_SANDWICH',
forPerson,
secretSauce,
};
}
function apologize(fromPerson, toPerson, error) {
return {
type: 'APOLOGIZE',
fromPerson,
toPerson,
error,
};
}
function withdrawMoney(amount) {
return {
type: 'WITHDRAW',
amount,
};
}
// Even without middleware, you can dispatch an action:
store.dispatch(withdrawMoney(100));
// But what do you do when you need to start an asynchronous action,
// such as an API call, or a router transition?
// Meet thunks.
// A thunk in this context is a function that can be dispatched to perform async
// activity and can dispatch actions and read state.
// This is an action creator that returns a thunk:
function makeASandwichWithSecretSauce(forPerson) {
// We can invert control here by returning a function - the "thunk".
// When this function is passed to `dispatch`, the thunk middleware will intercept it,
// and call it with `dispatch` and `getState` as arguments.
// This gives the thunk function the ability to run some logic, and still interact with the store.
return function(dispatch) {
return fetchSecretSauce().then(
(sauce) => dispatch(makeASandwich(forPerson, sauce)),
(error) => dispatch(apologize('The Sandwich Shop', forPerson, error)),
);
};
}
What we are doing here is just enabling redux to handle async code.

set user details in state after successful api call - Redux saga

I have a "My Profile" form that displays the details of the user.
Api call to fetch user data is as follows.
componentDidMount() {
this.props.getUserDetails();
}
Saga file is as follows
function* fetchUserDetails() {
try {
const response = yield call(userDetailsApi);
const user = response.data.user;
// dispatch a success action to the store
yield put({ type: types.USER_DETAILS_SUCCESS, user});
} catch (error) {
// dispatch a failure action to the store with the error
yield put({ type: types.USER_DETAILS_FAILURE, error });
}
}
export function* watchUserFetchRequest() {
yield takeLatest(types.USER_DETAILS_REQUEST, fetchUserDetails);
}
Reducer is as follows
export default function reducer(state = {}, action = {}) {
switch (action.type) {
case types.USER_DETAILS_SUCCESS:
return {
...state,
user: action.user,
loading: false
};
default:
return state;
}
}
Now i need to set the user details in state so that when the form values are changed, i can call the handleChange function to update the state.
If i had used redux thunk, i could have used something like as follows
componentDidMount() {
this.props.getUserDetails().then(() => {
this.setState({ user });
});
}
so that the user state contains all details of user and if a user property changes then the state can be updated using handleChange method .
That is,
After the api call, i need is something like
state = {
email: user#company.com,
name: 'Ken'
}
How to achieve the same using redux saga?

react-saga call function on source component

I'm pretty unsure how to ask this correctly. (I'm sorry) Basically I want to call the onError function in my Component with the Error String when the Saga function got a error. So I can fire up the Snackbar for 5 sec and then hide it again.
But I don't know how exactly I can call this Function from my Saga function. Currently it return the error on the this.state.error State as String. I tried to use componentWillReceiveProps but this doesn't work on the 2nd try when the string is still the same.
To avoid a xyproblem I'll just post the piece of code that I have.
I got the following component:
class RegisterForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
username: '',
password: '',
SnackbarOpen: false,
};
}
onSubmit = (event) => {
event.preventDefault();
this.props.register(this.state.email, this.state.username, this.state.password);
}
onError(error) {
this.setState({SnackbarOpen: true})
setTimeout(() => {
this.setState({SnackbarOpen: false});
}, 5000);
}
render(): with <form>
}
const mapStateToProps = (state) => ({
error: state.auth.error,
});
const mapDispatchToProps = (dispatch) => ({
register: (email, username, password) => {
dispatch(Actions.register(email, username, password));
}
});
export default connect(mapStateToProps, mapDispatchToProps)(RegisterForm);
Which call this Redux-Saga Function:
import { Types } from './Actions';
import CognitoService from './CognitoService';
function* register(action) {
try {
const result = yield call(CognitoService.register, action);
yield put({ type: Types.registrationSuccess, user: result.user });
} catch(error) {
yield put({ type: Types.registrationFail, error: error.message });
}
}
function* authSaga() {
yield takeLatest(Types.register, register);
}
export default authSaga;
Add a switch case to your auth reducer to match on the action type: Types.registrationFail. It should then pull out the registered error message and assign it to the auth.error field in your auth state. e.g.
authReducer(prevState, action){
...
switch(action.type){
case Types.registrationFail:
return {
...prevState,
error: action.error
};
}
...
}
Your component will pick up the store change via the connect(..) function. Then simply update your component with the componentWillReceiveProps lifecycle method to check the value of this message. e.g.
componentWillReceiveProps(nextProps, nextState){
if(nextProps.error != null && ! nextState.SnackbarOpen){
this.onError();
}
}
Making the assumption here that your snackbar is in within this component as well and simply pull its display text from the this.props.error value. Otherwise, there is scope to clean this up a bit more.
In this situation I see two solutions. The first one is more preferable, I think, with redux saga usual approach.
Rendering based on the store values
In your example you save "SnackbarOpen" variable on state level.
this.setState({SnackbarOpen: true})
Instead you can have a peace of store for the "register" component and save that variable there. So in such case, saga will change that value in the store on error. Simple example is:
function* register(action) {
try {
const result = yield call(CognitoService.register, action);
yield put({ type: Types.registrationSuccess, user: result.user });
} catch(error) {
yield put({ type: Types.registrationFail, error: error.message });
yield put({ type: Types.registrationSnackBarOpen });
yield delay(5000);
yield put({ type: Types.registrationSnackBarClose });
}
}
And, of course, bind that value to your component.
Adding callbacks
I don't recommend to use such approach, but it still exists. You can just add callbacks to your actions and call them in sagas. For example:
Component:
this.props.register(this.state.email, this.state.username, this.state.password, this.onError.bind(this);
Saga:
function* register(action) {
try {
const result = yield call(CognitoService.register, action);
yield put({ type: Types.registrationSuccess, user: result.user });
} catch(error) {
yield put({ type: Types.registrationFail, error: error.message });
action.onError();
}
}

Resources