Redux Saga - all the watches called for a dispatch action - reactjs

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

Related

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 is not triggering when action type is dispatched

I've implemented Redux-saga for the first time. I followed the documentation to create the saga.
The problem is Saga isn't triggering when the action type is dispatched. Let me elaborate a little.
I have a saga.js file that contains the watcher and worker saga in it. The root-saga.js just combines all the different sagas. The store.js file contains the basic setup code for the configuration for the sagas.
I'm dispatching the action type on a useEffect call in a component. The redux dev tool shows the action type is called correctly. Please have a look at the code.
The expected outcome is, the saga should automatically get called when the action type function is dispatched. The corresponding console logs should be shown.
Edit 1: Action definitions code are added
// store.js
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers/root-reducer';
import rootSaga from './sagas/root-saga';
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const store = createStore(
rootReducer,
compose(
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(),
applyMiddleware(...middlewares)
)
);
sagaMiddleware.run(rootSaga);
export default store;
// root-saga.js
import { all, call } from 'redux-saga/effects';
import { dashboardSagas } from './dashboard-sagas';
function* rootSaga() {
yield all([call(dashboardSagas)]);
}
export default rootSaga;
// saga.js page
import { takeLatest, put, call, all } from 'redux-saga/effects';
import axios from 'axios';
import { DASHBAORD } from '../types/dashboard';
import { DashboardSuccess, DashboardFail } from '../actions/dashboard';
import { APIS_ENDPOINTS } from '../../util/api-endpoints';
// watcher saga
export function* fetchDashboardSaga() {
yield takeLatest(DASHBAORD.START, fetchDashboardAsyncSaga);
}
// worker saga
function* fetchDashboardAsyncSaga() {
yield console.log('123');
try {
const resp = yield axios.get(APIS_ENDPOINTS.DASHBOARD);
yield console.log(resp);
yield put(DashboardSuccess(resp));
} catch (error) {
console.log(error);
yield put(DashboardFail('Something went wrong. Please try again'));
}
}
export function* dashboardSagas() {
yield all([call(fetchDashboardSaga)]);
}
// app.js
import { dashboardStart } from './redux/actions/dashboard';
function App() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(dashboardStart());
}, [dispatch]);
return (
// some dom
);
}
// Action definition
import { DASHBAORD } from '../types/dashboard';
const dashboardStart = () => {
return {
type: DASHBAORD.START,
};
};
const DashboardSuccess = (param) => {
return {
type: DASHBAORD.SUCCESS,
payload: param,
};
};
const DashboardFail = (param) => {
return {
type: DASHBAORD.FAILED,
payload: param,
};
};
export { dashboardStart, DashboardSuccess, DashboardFail };
Thank you
I found the issue. It was some weird glitch with the compose function in the store.js for redux. I removed that and installed the redux-devtools-extension library. Working fine now.
// before
compose(
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(),
applyMiddleware(...middlewares)
)
// after
composeWithDevTools(applyMiddleware(...middlewares))
If the actions is dispatched properly then the problem lies in the registration of your sagas.
Try replacing this code block
function* rootSaga() {
yield all([call(dashboardSagas)]);
}
with:
function* rootSaga() {
yield all([
// manually call the sagas. Only watcher functions. example:
dashboardSagas.saga1(),
dashboardSagas.saga2()
]);
}

Fetch data with redux saga

I have created an example for fetching data from API where I used redux-thunk. The following code is working.
In this context, I want to rewrite my code but using redux saga.
import React from 'react';
import {createStore, applyMiddleware} from 'redux';
import ReactDOM from "react-dom";
import thunk from 'redux-thunk';
import axios from 'axios';
function App(props) {
const initialState = {
loading: false,
data: [],
error: ''
};
const reducer = function (state = initialState, action) {
switch (action.type) {
case 'START_FETCH':
return {
...state,
loading: true
};
case 'PROCESS_FETCH':
return {
...state,
loading: false,
data: action.payload,
error: ""
};
case 'END_FETCH':
return {
...state,
loading: false,
data: [],
error: action.payload
}
}
return state;
};
const START_FETCH = 'START_FETCH';
const PROCESS_FETCH = 'PROCESS_FETCH';
const END_FETCH = 'END_FETCH';
let startFetchFun = () => {
return {
type: START_FETCH,
loading: true
}
};
let processFetchFun = (users) => {
return {
type: PROCESS_FETCH,
payload: users
}
};
let endFetchFun = (error) => {
return {
type: PROCESS_FETCH,
payload: error
}
};
let fetchUsersWithThunk = () => {
return function (dispatch) {
dispatch(startFetchFun());
axios.get('https://jsonplaceholder.typicode.com/users')
.then((response) => {
dispatch(processFetchFun(response.data));
})
.catch((error) => {
dispatch(endFetchFun(error.message));
console.log(error.message);
})
}
};
const store = createStore(reducer, applyMiddleware(thunk));
store.subscribe(() => {
console.log(store.getState())
});
store.dispatch(fetchUsersWithThunk());
return (
<div className="main">
<h1>Redux-Thunk</h1>
</div>
);
}
ReactDOM.render(
<App/>, document.getElementById('root'));
I want to write the code above using redux saga, to understand better sagas. So, how to use redux-saga for this example? Who will be able to help me?
Redux Saga uses yield call to call promises like an api service and uses yield put to dispatch actions to the store.
The difference is about blocking and not blocking calls.
Because we want to wait for the server to respond our request we will use yield call that is a blocking function.
Instead of dispatching the action directly inside the generator saga uses yield put({ type: "actionName" }). That's also useful for testing purposese.
So you should wrote your saga as following:
import {all, fork, put, call, takeLatest} from 'redux-saga/effects';
function* handleRequest (action) {
try {
yield put(startFetchFunc()); // dispatch the action to the store.
const result = yiels call(apiService.users, [data to pass]); // wait for the response blocking the code execution.
yield put(processFetchFun(result)); // dispatch the action to the store containing the data
} catch (e) {
yield put(endFetchFun('Error'));
}
}
function* watchRequest() {
yield takeLatest({type: "START_FETCH"}, handleRequest);
}
export function* rootSaga() {
yield all([
fork(wathcRequest),
// ... more watchers will be here...
]);
}
Congfigure you store as explained here https://redux-saga.js.org/docs/introduction/BeginnerTutorial.html
I suggest you to read the documentation more than once. It contains a lot of useful information that at first might be strange but much clearer once you understand how it works.
You will need to configure your store to use saga middleware:
import React from 'react';
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware, logger),
);
sagaMiddleware.run(rootSaga); // < -- rootSaga exports all sagas in your app
Then you can convert your thunk to a saga:
import {call} from 'redux-saga/effects';
function* fetchUsersSaga(payload){
try {
yield call(startFetchFun());
axios.get('https://jsonplaceholder.typicode.com/users')
.then((response) => {
yield call(processFetchFun(response.data));
})
} catch(err) {
yield call(endFetchFun(error.message));
console.log(error.message);
}
};

How to achieve callbacks in Redux-Saga?

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

Resources