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),
]);
}
Related
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.
all..
Iam new in react, I use react, redux and redux saga in my project. I just make simple api fetch using saga. but I don't know why the yield put command look did'nt work.
my code in bellow is my code:
types
export const GET_USERS_REQUESTED = "GET_USERS_REQUESTED";
export const GET_USERS_SUCCESS = "GET_USERS_SUCCESS";
export const GET_USERS_FAILED = "GET_USERS_FAILED";
actions
import * as type from "../types/users";
export function getUsers(users) {
return {
type: type.GET_USERS_REQUESTED,
payload: users,
};
};
reducers
import * as type from "../types/users";
const initialState = {
users: [],
loading: false,
error: null,
};
export default function users(state = initialState, action) {
switch (action.type) {
case type.GET_USERS_REQUESTED:
return {
...state,
loading: true,
};
case type.GET_USERS_SUCCESS:
return {
...state,
users: action.users,
loading: false,
};
case type.GET_USERS_FAILED:
return {
...state,
loading: false,
error: action.message,
};
default:
return state;
}
}
root reducers
import { combineReducers } from "redux";
import users from "./users";
const rootReducer = combineReducers({
users: users,
});
export default rootReducer;
user saga
import { call, put, takeEvery } from "redux-saga/effects";
import * as type from "../redux/types/users";
function getApi() {
return fetch("https://jsonplaceholder.typicode.com/users", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
response.json();
})
.catch((error) => {
throw error;
});
}
function* fetchUsers(action) {
try {
const users = yield call(getApi());
yield put({ type: type.GET_USERS_SUCCESS, users: users });
} catch (e) {
yield put({ type: type.GET_USERS_FAILED, message: e.message });
}
}
function* userSaga() {
yield takeEvery(type.GET_USERS_REQUESTED, fetchUsers);
}
export default userSaga;
root saga
import { all } from "redux-saga/effects";
import userSaga from "./users";
export default function* rootSaga() {
yield all([userSaga()]);
}
create store
import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from "./reducers/index";
import createSagaMiddleware from "redux-saga";
import rootSaga from "../sagas/index";
const sagaMiddleware = createSagaMiddleware();
const store = compose(
window.devToolsExtension && window.devToolsExtension(),
applyMiddleware(sagaMiddleware)
)(createStore)(rootReducer);
sagaMiddleware.run(rootSaga);
export default store;
I don't know why my user saga looks like did'nt work. because loading state still have true value. Hopefully, anyone can help me.
Thanks in advance
You are calling your getApi and returning promise to the call itself. That call will do that for your. So just provide call with getApi like this:
...
function* fetchUsers(action) {
try {
const users = yield call(getApi); // <-- HERE IS THE CHANGE
yield put({ type: type.GET_USERS_SUCCESS, users: users });
} catch (e) {
yield put({ type: type.GET_USERS_FAILED, message: e.message });
}
}
...
Also you need to change your getApi since you are using fetch:
async function getApi() {
const result = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => {
response.json();
})
.catch((error) => {
throw error;
});
return result;
}
If you need to provide a variable to your getApi call you can just do:
const users = yield call(getApi, 'someValue');
And you getApi looks something like this:
async function getApi(someValue) {
const result = await fetch(`https://jsonplaceholder.typicode.com/users/${someValue}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => {
return response.json();
})
.catch((error) => {
throw error;
});
return result;
}
When you use call you should not call() it. Type yield call(getApi, "in case you have arguments"). You should do that, when you are waiting for a promise
I am using Redux for state management and saga as a middleware. For some reason my app is in some infinite loop state of calling API endpoint.
This is my actions:
export const GET_USERS = "GET_USERS";
export const getUsers = () => ({
type: GET_USERS,
});
export const GET_USERS_SUCCESS = `${GET_USERS}_SUCCESS`;
export const getUsersSuccess = (data) => ({
type: GET_USERS_SUCCESS,
payload: data,
});
export const GET_USERS_FAIL = `${GET_USERS}_FAIL`;
export const getUsersFail = (error) => ({
type: GET_USERS_FAIL,
payload: error,
});
This is saga:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.getUsers, getUsers$)]);
}
This is a reducer:
export default (state = initialState(), action) => {
const { type, payload } = action;
switch (type) {
case actions.GET_USERS:
return {
...state,
users: {
...state.users,
inProgress: true,
},
};
case actions.GET_USERS_SUCCESS:
return {
...state,
users: {
inProgress: false,
data: payload,
},
};
case actions.GET_USERS_FAIL:
return {
...state,
users: {
...state.users,
inProgress: false,
error: payload,
},
};
default:
return state;
}
};
And this is a component connected with redux:
const Home = (props) => {
useEffect(() => {
props.getUsers();
console.log('props', props.data);
}, []);
return(
<h1>Title</h1>
);
}
const mapStateToProps = ({
users: {
users: {
data
}
}
}) => ({data})
export default connect(mapStateToProps, {getUsers})(Home);
Why is this happening?
This is due to the fact that you misused the sagas in your example. As with any other effect creator as the first parameter must pass a pattern, which can be read in more detail in the documentation. The first parameter can also be passed a function, but in a slightly different way. View documentation (block take(pattern)).
In your case, you are passing a function there that will return an object
{
type: 'SOME_TYPE',
payload: 'some_payload',
}
Because of this, your worker will react to ALL events that you dispatch.
As a result, you receive data from the server, dispatch a new action to save data from the store. And besides the reducer, your getUsers saga will be called for this action too. And so on ad infinitum.
Solution
To solve this problem, just use the string constant actions.GET_USERS that you defined in your actions.
And your sagas will look like this:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.GET_USERS, getUsers$)]);
}
This should fix your problem.
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);
}
};
Until now I've been using redux-thunk for async actions. On application startup I use to have to load some data from some server. So what I do is to create async actions and then use async/await in order to know when they finished. While async actions are fetching I render a splashscreen. When they finish then I start the application.
Now I'm switching to redux sagas and I don't know how to do it with them. I cannot use async/await. What I thought is to have a boolean var in every object of the store that needs to fetch data. However I would like to know if there is any pattern to manage it in a clean way. Does anybody know any pattern for this purpose?
// example with thunks
import { someAsyncAction, someAsyncAction2 } from './actions';
const initialDispatches = async (store) => {
await store.dispatch(someAsyncAction());
await store.dispatch(someAsyncAction2());
};
export default initialDispatches;
In my opinion there's no right/wrong pattern in this kind of cases.
Iv'e put up an example for you of how your goal could be achieved using saga.
The basic idea: have a separate saga for each resource (for instance, I used to split into feature sagas), and a saga for the initialization.
Then the main root saga will run them all parallelly, and you will be able to trigger the initialization saga somewhere in your app and let it all happen:
Note: this example is super naive and simple, you should find a better way for organizing everything up, I just tried to keep it simple.
const {Provider, connect} = ReactRedux;
const {createStore, applyMiddleware} = Redux;
const createSagaMiddleware = ReduxSaga.default;
const {takeEvery, takeLatest} = ReduxSaga;
const {put, call, all, fork} = ReduxSaga.effects;
const initialState = {
fruits: [],
vegtables: []
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_FRUITS':
return {
...state,
fruits: [
...action.payload.fruits
]
}
case 'SET_VEGTABLES':
return {
...state,
vegtables: [
...action.payload.vegtables
]
}
}
return state;
};
//====== VEGTABLES ====== //
async function fetchVegtables() {
return await new Promise((res) => {
setTimeout(() => res([
'Cuecumber',
'Carrot',
'LEttuce'
]), 3000)
});
}
function* getVegtables() {
const vegtables = yield call(fetchVegtables);
yield put({ type: 'SET_VEGTABLES', payload: { vegtables } })
}
function* vegtablesSaga() {
yield takeEvery('GET_VEGTABLES', getVegtables);
}
//====== VEGTABLES ====== //
//====== FRUITS ====== //
async function fetchFruits() {
return await new Promise((res) => {
setTimeout(() => res([
'Banana',
'Apple',
'Peach'
]), 2000)
});
}
function* getFruits() {
const fruits = yield call(fetchFruits);
console.log(fruits)
yield put({ type: 'SET_FRUITS', payload: { fruits } })
}
function* fruitsSaga() {
yield takeEvery('GET_FRUITS', getFruits);
}
//====== FRUITS ====== //
//====== INIT ====== //
function* initData() {
yield all([
put({ type: 'GET_FRUITS' }),
put({ type: 'GET_VEGTABLES' })
]);
}
function* initSaga() {
yield takeLatest('INIT', initData);
}
//====== INIT ====== //
// Sagas
function* rootSaga() {
yield all([
yield fork(initSaga),
yield fork(fruitsSaga),
yield fork(vegtablesSaga),
]);
}
// Component
class App extends React.Component {
componentDidMount() {
this.props.dispatch({ type: 'INIT' });
}
render () {
return (
<div>
<div>fruits: {this.props.fruits.join()}</div>
<div>vegtables: {this.props.vegtables.join()}</div>
</div>
);
}
}
// Store
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);
const ConnectedApp = connect((state) => ({
fruits: state.fruits,
vegtables: state.vegtables
}))(App);
// Container component
ReactDOM.render(
<Provider store={store}>
<ConnectedApp />
</Provider>,
document.getElementById('root')
);
As you can see, I have two resources: fruits and vegetables.
Each resource has it's own saga, which is responsible for watching for GET actions dispatched somewhere.
Each of them using basic saga effects such as call, put etc to fetch the resources asyncly, and then they dispatch it to the store (and then the reducer handles them).
In addition, Iv'e set up an initSaga which uses the all effect to trigger all of the resource fetching sagas in a parallel way.
You can see the whole example running here:
https://jsfiddle.net/kadoshms/xwepoh5u/17/
I wrote about creating a structure on top of redux-saga to facilitate async operations by providing an initial action and then loading/success/error states based on the result of the operation. It's in 2 parts, first sync and then async.
It basically lets you write your reducers declaratively, like an object. You only have to call the initial action and the saga takes care of the rest and your UI can respond to the results when loading/success/error actions are triggered. Below is what the reducer looks like.
const counterAsync = {
initialState: {
incrementAsync_result: null,
incrementAsync_loading: false,
incrementAsync_success: false,
incrementAsync_error: false,
},
incrementAsync: {
asyncOperation: incrementAPI,
action: ({number}) => {
type: ACTION_INCREMENT_ASYNC,
payload: {
number: number
}
}
loading: {
action: (payload) => {
return {
type: ACTION_INCREMENT_ASYNC,
payload: { ...payload }
}
},
reducer: (state, action) => {
state.incrementAsync_loading = true
state.incrementAsync_success = false
state.incrementAsync_error = false
}
},
success: {
action: (payload) => {
return {
type: ACTION_INCREMENT_ASYNC,
payload: { ...payload }
}
},
reducer: (state, action) => {
state.incrementAsync_result = action.payload
state.incrementAsync_loading = false
state.incrementAsync_success = true
state.incrementAsync_error = false
}
},
fail: {
action: (payload) => {
return {
type: ACTION_INCREMENT_ASYNC,
payload: { ...payload }
}
},
reducer: (state, action) => {
state.incrementAsync_result = action.payload
state.incrementAsync_loading = false
state.incrementAsync_success = false
state.incrementAsync_error = true
}
}
},
}
We use a slightly more heavy weight version of this pattern at work and it's much better than vanilla redux/saga.
Let me know if you have any questions!
https://medium.com/#m.razajamil/declarative-redux-part-1-49a9c1b43805
https://medium.com/#m.razajamil/declarative-redux-part-2-a0ed084e4e31