How to achieve callbacks in Redux-Saga? - reactjs

The scenario is, I want to redirect a user or show alert based on the success, error callbacks after dispatching an action.
Below is the code using redux-thunk for the task
this.props.actions.login(credentials)
.then((success)=>redirectToHomePage)
.catch((error)=>alertError);
because the dispatch action in redux-thunk returns a Promise, It is easy to act with the response.
But now I'm getting my hands dirty on redux-saga, and trying to figure out how I can achieve the same result as above code. since Saga's run on a different thread, there is no way I can get the callback from the query above. so I just wanted to know how you guys do it. or whats the best way to deal with callbacks while using redux-saga ?
the dispatch action looks like this :
this.props.actions.login(credentials);
and the saga
function* login(action) {
try {
const state = yield select();
const token = state.authReducer.token;
const response = yield call(API.login,action.params,token);
yield put({type: ACTION_TYPES.LOGIN_SUCCESS, payload:response.data});
yield call(setItem,AUTH_STORAGE_KEY,response.data.api_token);
} catch (error) {
yield put({type: ACTION_TYPES.LOGIN_FAILURE, error})
}
}
saga monitor
export function* loginMonitor() {
yield takeLatest(ACTION_TYPES.LOGIN_REQUEST,login);
}
action creator
function login(params) {
return {
type: ACTION_TYPES.LOGIN_REQUEST,
params
}
}

I spent all day dinking around with this stuff, switching from thunk to redux-saga
I too have a lot of code that looks like this
this.props.actions.login(credentials)
.then((success)=>redirectToHomePage)
.catch((error)=>alertError);
its possible to use thunk + saga
function login(params) {
return (dispatch) => {
return new Promise((resolve, reject) => {
dispatch({
type: ACTION_TYPES.LOGIN_REQUEST,
params,
resolve,
reject
})
}
}
}
then over in saga land you can just do something like
function* login(action) {
let response = yourApi.request('http://www.urthing.com/login')
if (response.success) {
action.resolve(response.success) // or whatever
} else { action.reject() }
}

I think you should add redirect and alert to the login generator. This way all logic is in the saga and it is still easily tested. So basically your login saga would look like this:
function* login(action) {
try {
const state = yield select();
const token = state.authReducer.token;
const response = yield call(API.login,action.params,token);
yield put({type: ACTION_TYPES.LOGIN_SUCCESS, payload:response.data});
yield call(setItem,AUTH_STORAGE_KEY,response.data.api_token);
yield call(redirectToHomePage); // add this...
} catch (error) {
yield put({type: ACTION_TYPES.LOGIN_FAILURE, error});
yield call(alertError); // and this
}
}

You can simply work up by passing the extra info about your success and error callback functions into the payload itself. Since, redux pattern works in a quite decoupled manner.
this.props.actions.login({
credentials,
successCb: success => redirectToHomePage)
errorCb: error => alertError)
});
In the saga, you can deconstruct these callbacks from the payload and run them very easily based on your program flow.

Your call:
this.props.addCutCallback(currentTime, callback);
Your mapping that you pass to connect() function:
const mapDispatchToProps = (dispatch) => ({
addCutCallback: (time, callback) =>
dispatch(ACTIONS.addCutCallback(time, callback)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Home);
Your saga:
import {put, takeEvery, all, select} from 'redux-saga/effects';
import * as Actions from './../actions';
const getCuts = (state) => state.cuts;
function* addCutSaga({time, callback}) {
yield put({type: Actions.ADD_CUT, time});
const cuts = yield select(getCuts);
callback(cuts);
}
function* cutsSaga() {
yield takeEvery(Actions.ADD_CUT_CALLBACK, addCutSaga);
}
export default function* rootSaga() {
yield all([cutsSaga()]);
}

An approach that I find more elegant is to simply use the useEffect.
//selectors.ts
export function hasAuthError(state: RootState): boolean {
return state.auth.error;
}
export function getAuthMessage(state: RootState): string {
return state.auth.message;
}
// some react component
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Toast from 'react-native-toast-message';
import { getAuthMessage, hasAuthError } from 'src/store/auth/selectors';
...
const message = useSelector(getAuthMessage);
const hasError = useSelector(hasAuthError)
...
useEffect(() => {
if (hasError) {
Toast.show({
type: 'error',
text2: message,
topOffset: 50,
visibilityTime: 5000
});
}
}, [message, hasError]);

Related

Redux saga is called twice with one dispatch

Im trying to comprehend the art of redux saga, but faced this situation:
I have useEffect hook that works correctly(works one time when changing url params). This hook dispatches action(created by redux-saga-routines) only one time.
const params = useParams().params;
useEffect(() => {
if (urlTriggers.some(item => item === params)) {
dispatch(setItemsCollection({ itemType: params }));
toggleVisibleMode(true);
} else {
toggleVisibleMode(false);
}
}, [params]);
Saga watcher reacts to the dispatched action
export function* setItemsCollectionWatcher() {
yield takeEvery(setItemsCollection.TRIGGER, setItemsCollectionWorker);
}
And then calls saga worker
function* setItemsCollectionWorker(action) {
const { itemType } = action.payload;
try {
yield put(toggleIsFetching({ isFetching: true }));
const itemsCollection = yield call(() => {
return axios.get(`http://localhost:60671/api/items/${itemType}/?page=1&count=2`).then(response => response.data.items);
});
yield put(setItemsCollection.success({ itemsCollection }));
yield put(toggleIsFetching({ isFetching: false }));
} catch (error) {
console.log(error);
} finally {
yield put(setItemsCollection.fulfill());
}
}
This saga listens all saga watchers
export default function* saga() {
yield all([
setBackgroundWatcher(),
setItemsCollectionWatcher(),
])
}
saga running
sagaMiddleware.run(saga);
export const setItemsCollection = createRoutine('showcase/SET_ITEMS_COLLECTION');
export const toggleIsFetching = createRoutine('showcase/TOGGLE_IS_FETCHING');
const showcase = createReducer(
{
itemsCollection: [],
isFetching: false,
},
{
[setItemsCollection.SUCCESS]: (state, action) => {
state.itemsCollection = action.payload.itemsCollection;
},
[toggleIsFetching.TRIGGER]: (state, action) => {
state.isFetching = action.payload.isFetching;
},
}
);
But I have 2 axios requests instead of just one.
Your dispatch from the client is type setItemsCollection which should be fine (though I generally 'USE_HUGE_OBVIOUS_TEXT_LIKE_THIS'). The response from the promise in your saga is the same: setItemsCollection whereas, depending on what you're trying to render, you may want to call your reducer with something entirely different.
At a glance, I'd suggest changing this line to something else (and matching what the reducer is listening for). I wonder if it's causing a crossed wire somewhere.

How to run dispatch on a saga function when a button is clicked?

I have a use case where I might be dispatching actions(saga) later in the game. I have an application where user fills out a form and later on when user clicks on submit that time the saga action will be called through which i will get response from api. But i also have another worker saga that run in the initialization time of application. Somehow only initialize works other forks don't work -
sagas/applicationSaga.js
export function* initialize() {
yield call(getAppInfo);
}
export function* getAppInfo() {
try {
const appInfo = (yield call(
AppApi.getAppInfo
)).data;
} catch (e) {
throw new Error(e);
}
}
export function* submitDecision({payload}) {
try {
const submitDecision = yield call(AppApi.submitDecision, payload)
} catch (e) {
throw new Error(e);
}
}
export function* applicationSaga() {
yield fork(takeEvery, ActionTypes.INITIALIZATION_REQUESTED, initialize);
yield fork(takeEvery, ActionTypes.SUBMIT_DECISION, submitDecision)
}
sagas/index.js
import { applicationSaga, initialize } from "./applicationSaga";
export default function* rootSaga() {
yield all([applicationSaga(), initialize()]);
}
This is a method to invoke a generator function in Saga. Store, actions and reducers must be implemented properly in order to make the application be in sync with the data. Please have a look at How do I call a Redux Saga action in a `onClick` event? this.
yourJS.js
import store from "YOUR_LOCATION/store";
submit = () => {
store.dispatch({type: "SUBMIT", payload: YOUR_PAYLOAD});
}
<button onClick={this.submit}>Submit</button>
SAGA.js
import { takeLatest } from "redux-saga/effects";
export const watchSubmit = function* () {
yield takeLatest("SUBMIT", workerSubmit);
};
function* workerSubmit(action) {
try {
// Your code
} catch {
message.error("Failed")
}
}
I got the way to implement this -
export function* initialize() {
yield take(ActionTypes.INITIALIZATION_REQUESTED);
yield call(getAppInfo);
}
export function* submitDecision({payload}) {
try {
const submitDecision = yield call(AppApi.submitDecision, payload)
} catch (e) {
throw new Error(e);
}
}
export function* applicationSaga() {
yield all([
initialize(),
takeEvery(ActionTypes.SUBMIT_DECISION, submitDecision)
]);
}
sagas/index.js
import { all } from "redux-saga/effects";
import { applicationSaga } from "./applicationSaga";
export default function* rootSaga() {
yield all([applicationSaga()]);
}
Basically I had to yield all in root saga and in my actual application saga I can keep adding workers attaching to my watcher saga.

Dispatch action from saga in order to reach reducer while that action is being watched by the same saga

When I run getUser() from my component in order to dispatch a GET_USER type action the action is being caught by my saga. Then by using getUserInfo() I make a request to fetch some data and in order to update the state, I dispatch a GET_USER_SUCCESS with the results as payload. Is this a bad approach?
Can I somehow dispatch GET_USER again from within the saga passing the results as payload without having the saga intercept it so it can reach the reducer?
actionTypes.js
export const GET_USER = "GET_USER"
export const GET_USER_SUCCESS = "GET_USER_SUCCESS"
export const GET_USER_FAIL = "GET_USER_FAIL"
actions.js
import {
GET_USER,
GET_USER_SUCCESS,
GET_USER_FAIL
} from "./actionTypes"
export const getUser = () => ({
type: GET_USER,
})
export const getUserSuccess = data => ({
type: GET_USER_SUCCESS,
payload: data,
})
export const getUserFail = error => ({
type: GET_USER_FAIL,
payload: error,
})
saga.js
import { call, put, takeEvery } from "redux-saga/effects"
import { GET_USER } from "./actionTypes"
import { getUser, getUserSuccess, getUserFail } from "./actions"
import { getUserInfo } from "../../../helpers/backend_helper"
function* getUserInformation() {
try {
const user_response = yield call(getUserInfo)
if (user_response && user_response.id)
yield put(getUserSuccess(user_response))
else
yield put(getUserFail('could not fetch user'))
}
catch (error) {
yield put(apiError("Bad response from server"))
}
}
function* userSaga() {
yield takeEvery(GET_USER, getUserInformation)
}
export default userSaga
backend_helper.js
import { post, del, get, put } from "./api_helper"
const getUserInfo = () => get("/users/me/")
I think what you're suggesting also makes things harder to debug. You would want unique actions; GET_USER to tell you that you're fetching data and GET_USER_SUCCESS to tell you that you've successfully received the information.
In complex web applications you could see up to 20, 30, even 40 actions so being able to distinguish them is vital in my opinion.

About the import method of redux saga

I am a 2-month front end developer.
I studied React at the same time as I joined the company,
so there are many parts I do not know well.
I am currently analyzing the code to proceed with code maintenance,
but there is an esoteric part of the code.
First, In the saga, There is no part where the action function is imported. (even in the root saga.)
So, Is it possible to implicitly import in the code?
I'm attaching some of the code to help you understand.
rootSaga.js
import { all } from "redux-saga/effects";
import watcherLogin from "store/sagas/login";
import watcherSignUp from "store/sagas/signup";
export default function* rootSaga() {
yield all([
watcherLogin(),
watcherSignUp(),
]);
}
watcherLogin() > index.js
export { default } from "./watcherLoginSaga"
watcherLogin() > watcherLoginSaga.js
import { all, put, fork, takeLatest } from "redux-saga/effects";
import Cookies from "universal-cookie";
import { fetchData } from "store/sagas/baseSaga";
function* onRequestLogin(action) {
const payload = action.payload;
const { history } = payload;
const successAction = (res) => {
const cookies = new Cookies();
cookies.set("hdmKey", res.data, {
path: "/",
maxAge: 3600,
});
return function* () {
const payload = res;
yield put({
type: "login/GET_USERINFO_REQUEST",
payload: {
method: "get",
api: "getUserInfo",
// token: res.data.key,
history,
},
});
yield put({
type: "login/LOGIN_REQUEST_SUCCESS",
payload,
});
yield put({
type: "base/IS_LOGGED",
payload,
});
yield history.push("/");
};
};
const failureAction = (res) => {
return function* () {
yield put({
type: "base/SHOW_MODAL",
payload: {
dark: true,
modalName: "alert",
modalContent: "login failure",
modalStyle: "purpleGradientStyle",
modalTitle: "Wait!",
},
});
};
};
yield fork(fetchData, payload, successAction, failureAction);
}
...
export default function* watcherLoginSaga() {
yield all([
takeLatest("login/LOGIN_REQUEST", onRequestLogin),
]);
}
loginModule > index.js
export { default } from "./loginModule";
loginModule > loginModule.js
import createReducer from "store/createReducer";
import { changeStateDeep } from "lib/commonFunction";
export const types = {
LOGIN_REQUEST: "login/LOGIN_REQUEST",
LOGIN_REQUEST_SUCCESS: "login/LOGIN_REQUEST_SUCCESS",
...
};
export const actions = {
loginRequest: (payload) => ({
type: types.LOGIN_REQUEST,
payload,
}),
...
};
const INITIAL_STATE = {
data: {
isLogged: false,
...
},
};
const reducer = createReducer(INITIAL_STATE, {
[types.ON_LOGIN]: (state, action) => {
state.data.isLogged = true;
},
[types.LOGIN_REQUEST_SUCCESS]: (state, action) => {
state.data.isLogged = true;
state.data.key = action.payload?.key || "key";
},
...
});
export default reducer;
I would appreciate it if you could help even a little.
An action is just a plain javascript object with a property type. As a convention (but not a strict requirement), action objects store their data in an optional property payload.
An actionCreator is a function which takes 0 to many arguments and uses them to create an action object.
What you are seeing in the code that you posted works, but it's not ideal. I'm guessing that certain improvements were made to the loginModule.js file and those improvements were not brought over to the watcherLoginSaga.js file.
Your types object allows you to use a variable such as types.LOGIN_REQUEST rather than the raw string "login/LOGIN_REQUEST". This has a bunch of benefits:
you get intellisense support in your IDE for autocomplete, etc.
you don't have to worry about making typos
you can change the value of the underlying raw string and you just need to change it one place
That last one is critical, because if you were to change the value of types.LOGIN_REQUEST to anything other than "login/LOGIN_REQUEST" right now your sagas would stop working because they are using the raw string. So you are absolutely right in thinking that the saga should import from the actions. I recommend that you import your types and replace the strings with their corresponding variables.
The same situation is happening with the actions that the saga is dispatching via put.
What's going on in this code is that the saga is creating a raw action object itself from the type and payload rather than creating it though an action creator function. That's fine but it's not great. Like the types, action creators are a level of abstraction that allows for better code maintenance. You could definitely extract the logic from your saga into action creator functions, if they don't exist already.
For example, your base module could include:
const types = {
IS_LOGGED: "base/IS_LOGGED",
SHOW_MODAL: "base/SHOW_MODAL"
};
export const actions = {
isLogged: (payload) => ({
type: types.IS_LOGGED,
payload
}),
showLoginFailure: () => ({
type: types.SHOW_MODAL,
payload: {
dark: true,
modalName: "alert",
modalContent: "login failure",
modalStyle: "purpleGradientStyle",
modalTitle: "Wait!"
}
})
};
And you can move the logic for creating actions away from your saga.
import { all, put, fork, takeLatest } from "redux-saga/effects";
import Cookies from "universal-cookie";
import { fetchData } from "store/sagas/baseSaga";
import {actions as loginActions, types as loginTypes} from "../some_path/loginModule";
import {actions as baseActions} from "../some_path/baseModule";
function* onRequestLogin(action) {
const payload = action.payload;
const { history } = payload;
const successAction = (res) => {
const cookies = new Cookies();
cookies.set("hdmKey", res.data, {
path: "/",
maxAge: 3600,
});
return function* () {
const payload = res;
yield put(loginActions.getUserInfoSuccess(history));
yield put(loginActions.loginSuccess(payload));
yield put(baseActions.isLogged(payload));
yield history.push("/");
};
};
const failureAction = (res) => {
return function* () {
yield put(baseActions.showLoginFailure());
};
};
yield fork(fetchData, payload, successAction, failureAction);
}
export default function* watcherLoginSaga() {
yield all([
takeLatest(loginTypes.LOGIN_REQUEST, onRequestLogin),
]);
}

Redux Saga - all the watches called for a dispatch action

I have declared the following saga api.
export function* watchSaveProducts() {
yield takeLatest(ProductActionTypes.PRODUCT_SAVE_REQUEST, saveProducts);
}
export const saga = function* productSagasContainer() {
yield all([watchGetProducts(), watchSaveProducts()]);
};
When I dispatch an action from the container, both the saga watches triggered. But in that I am just calling only getProducts. Even If I do save products, then getProducts triggered before save products.
componentDidMount() {
this.props.getProducts();
}
The dispatch props like follow
const mapDispatchToProps = (dispatch: any) => ({
getProducts: () => dispatch(productActions.getProducts()),
saveProduct: (ids: number[]) => dispatch(productActions.saveProduct(ids)),
});
You're throwing both methods when you do this:
export const saga = function* productSagasContainer() {
yield all([watchGetProducts(), watchSaveProducts()]);
};
Therefore, both will always be running.
I'll explain my structure when I work with redux and sagas:
First, create a sagaMiddleware to connect redux-store with sagas (extracted from the redux-saga documentation) :
import createSagaMiddleware from 'redux-saga'
import reducer from './path/to/reducer'
export default function configureStore(initialState) {
// Note: passing middleware as the last argument to createStore requires redux#>=3.1.0
const sagaMiddleware = createSagaMiddleware()
return {
...createStore(reducer, initialState, applyMiddleware(/* other middleware, */sagaMiddleware)),
runSaga: sagaMiddleware.run(rootSaga)
}
}
Define rootSaga with sagas divided into application parts:
export function* rootSaga() {
yield all([fork(sample1Watcher), fork(sample2Watcher)]);
}
Create the set of sagas that will be launched in this part depending on the action that is dispatched
export function* sample1Watcher() {
yield takeLatest(ProductActionTypes.GET_PRODUCT_REQUEST, getProduct);
yield takeLatest(ProductActionTypes.PRODUCT_SAVE_REQUEST, saveProduct);
}
Define each of the methods, for example the get:
function* getProduct() {
try {
yield put({ type: actionTypes.SET_PRODUCTS_LOADING });
const data = yield call(products.fetchProductsAsync);
yield put({ type: actionTypes.GET_PRODUCTS_COMPLETE, payload: data });
} catch (e) {
console.log("Error");
}
}
Finally, define action method in dispatchToProps and launch it wherever you want:
const mapDispatchToProps = (dispatch: any) => ({
getProducts: () => dispatch(productActions.getProducts()),
saveProduct: (ids: number[]) => dispatch(productActions.saveProduct(ids)),
});
componentDidMount() {
this.props.getProducts();
}

Resources