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

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?

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.

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

Manipulating Redux output

I'm working on a React application to render content of a WordPress website using the WordPress Rest API, Redux and Thunk.
The WordPress API returns posts without detailed information about the category (name, slug, etc). All I'm getting is the id. I'm currently calling an additional action/function to get the detailed category information (output). Below an example of how I'm currently fetching my posts.
// Actions.js
import axios from 'axios'
export const fetchPosts = (page = 1) => {
return {
type: "FETCH_POSTS",
payload: axios.get(`${REST_URL}/wp/v2/posts?per_page=14&page=${page}`)
}
}
|
// PostsReducer.js
const initialState = {
posts: [],
fetching: false,
fetched: false,
error: null
}
export default function reducer(state=initialState, action) {
switch (action.type) {
case "FETCH_POSTS": {
return {
...state,
fetching: true
}
}
case "FETCH_POSTS_REJECTED": {
return {
...state,
fetching: false,
error: action.payload
}
}
case "FETCH_POSTS_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
posts: action.payload
}
}
}
return state
}
And this is how I'm fetching category information:
export const fetchCategory = (id) => {
return {
type: "FETCH_CATEGORY",
payload: axios.get(`${REST_URL}/wp/v2/categories/${id}`)
}
}
Is there a way to combine my fetchPosts() action with the fetchCategory() action, so it populates the post.categories, returned from fetchPosts() with the more detailed fetchCategory() information?
If you're referring for ajax calls chaining you can use this example to understand how thunk can work for you:
function loadSomeThings() {
return dispatch => {
fetchFirstThingAsync.then(data => { // first API call
dispatch({ type: 'FIRST_THING_SUCESS', data }); // you can dispatch this action if you want to let reducers take care of the first API call
return fetchSecondThingAsync(data), // another API call with the data received from the first call that returns a promise
})
.then(data => {
dispatch({ type: 'SECOND_THING_SUCESS', data }); // the reducers will handle this one as its the object they are waiting for
});
};
}
Basically when we call loadSomeThings we dispatch an new action as a function (fetchFirstThingAsync) as our first ajax call, redux-thunk will catch that before any reducer does as function are not the plain object that reducers can handle, thunk will invoke this function with dispatcher as an argument (along getState and some more args), we wait it out with .then and then we can dispatch a plain object that reducers can handle + returning another promise (fetchSecondThingAsync) that's your second ajax call, we wait it out with .then and again dispatching a plain object that reducers can handle.

Thunks in React-Redux - Not Resolving?

I am using React and Redux to create a login system with Google Firebase. I am trying to understand how to use thunks. I am calling my action createUser from my React component however, I'm not able to handle the callback successfully.
Here is the component function I am calling the action from:
registerUser() {
let email = this.state.user.email;
let pw= this.state.user.password;
this.props.actions.createUser(email, pw)
.then((user) => {
debugger; // The async request is successful but execution doesn't pause here
})
.catch((error) => {
debugger; // Instead I receive an error here that says, "Uncaught (in promise) RangeError: Maximum call stack size exceeded"
});
}
Here are the actions:
export function createUserSuccess(user) {
debugger;
return { type: types.CREATE_USER_SUCCESS, payload: { registeredUser: user, registerMsg: 'Successful Registration!' }};
}
export function createUserError(error) {
return { type: types.CREATE_USER_ERROR, payload: { registeredUser: {}, registerMsg: error.message }};
}
export function createUser(email, pw) { // async thunk
debugger;
return (dispatch) => {
debugger;
return firebase.auth().createUserWithEmailAndPassword(email, pw)
.then((user) => {dispatch(createUserSuccess(user))}) // todo: figure out why this won't resolve
.catch(error => dispatch(createUserError(error)));
}
}
And my Reducer:
import * as types from '../actions/actionTypes';
import initialState from './initialState';
export default function registerReducer(state = initialState.registeredUser, action) {
debugger;
switch (action.type) {
case types.CREATE_USER_SUCCESS:
return [
...state, // es6 spread operator - explodes all values in array
Object.assign({}, action.payload)
];
case types.CREATE_USER_ERROR:
return [
...state,
Object.assign({}, action.payload)
];
default:
return state;
}
}
I know the actual request to Google firebase is OK because the createUserSuccess action creator gets fired. Why isn't execution stopping at the appropriate place in my React Component?
You can check here this implementation
The Service when we read the user auth and set the value to Redux
https://github.com/x-team/unleash/blob/develop/app/services/authService.js
The reducer when set the user state to the redux state object
https://github.com/x-team/unleash/blob/develop/app/reducers/userReducer.js
The action creators
https://github.com/x-team/unleash/blob/develop/app/actions/UserActions.js
The most important part is the authService, let me know any question

Promise.catch in redux middleware being invoked for unrelated reducer

I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
An error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillUpdate of the parent component but for some reason the catch handler in the middleware from the first code snippet is invoked:
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I do not understand why the catch handler is being invoked as I would have thought the promise has resolved at this point.
Am not completely sure, it's hard to debug by reading code. The obvious answer is because it's all happening within the same stacktrace of the call to next(actionWith({ type: types.SUCCESS, payload: { response } })).
So in this case:
Middleware: Dispatch fetchTeam success inside Promise.then
Redux update props
React: render new props
React: componentWillMount
React: Dispatch new action
If an error occurs at any point, it will bubble up to the Promise.then, which then makes it execute the Promise.catch callback.
Try calling the autocomplete fetch inside a setTimeout to let current stacktrace finish and run the fetch in the next "event loop".
setTimeout(
() => this.props.dispatch(actions.init({ props: this.exportProps() }))
);
If this works, then its' the fact that the event loop hasn't finished processing when the error occurs and from the middleware success dispatch all the way to the autocomplete rendered are function calls after function calls.
NOTE: You should consider using redux-loop, or redux-saga for asynchronous tasks, if you want to keep using your custom middleware maybe you can get some inspiration from the libraries on how to make your api request async from the initial dispatch.

Resources