Redux saga dispatching actions from inside map inside saga-action - reactjs

I have API calls in the following manner:
Call main api which gives me an array of objects.
For each object inside array I have to call another API asynchronously.
As soon as the sub API call for the object is finished, update its data in redux store which is an array (ofc) and show it.
So the scenario is a list showing items which grow in dynamic fashion.
Since I am using redux-saga, I have to dispatch the second part from inside an redux-action. I have tried the follow way:
const response = yield call(get, 'endpoint')
const configHome = response.map(function* (ele) {
const data = yield call(get, ele.SomeURI + '?someParameter=' + ele.someObject.id)
}))
This doesn't work since map doesn't know anything about generator functions. So I tried this:
const response = yield call(get, 'endpoint')
const configHome = yield all(response.map((ele) => {
return call(get, paramsBuilder(undefined, ele.CategoryID))
}))
But this will block my UI from showing available data untill all sub API calls are finished.
I have also tried making a separate generator function which I call from inside map and call its .next() function but the problem here again is that saga doesn't control that generator function so the call effect doesn't return any value properly.
Completely stuck on this part. Would appreciate any help.

Have you tried this, I have created a sample this might help
import { put, takeLatest, all, call } from 'redux-saga/effects';
function* _fetchNews(id) {
const data = yield fetch(
`https://jsonplaceholder.typicode.com/todos/${id}`
).then(function(response) {
const data = response.json();
return data;
});
console.log(id);
yield put({ type: 'NEWS_RECEIVED', data });
return data;
}
function* _getData() {
const json = yield fetch('https://jsonplaceholder.typicode.com/todos').then(
response => response.json()
);
return json;
}
function* fetchNews() {
const json = yield _getData();
const configHome = json.map(ele => _fetchNews(ele.id));
for (var item of configHome) {
yield item;
}
}
function* actionWatcher() {
yield takeLatest('GET_NEWS', fetchNews);
}
export default function* rootSaga() {
yield all([actionWatcher()]);
}

yield all - the generator is blocked until all the effects are resolved or as soon as one is rejected
So you will need to dispatch events separately for each sub API
Let's assume you have 2 actions:
export const getMainApi =() => ({
type: types.GET_MAIN_API,
});
export const getSubApi = endpoint => ({
type: types.GET_SUB_API,
endpoint,
});
Then your operations will be:
const get = endpoint => fetch(endpoint).then(response => response);
function* fetchMainApi(action) {
try {
const response = yield call(get, 'endpoint');
for (let i = 0; i < response.length; i += 1) {
// dispatch here all sub APIs
yield put(
getSubApi(
response[i].SomeURI + '?someParameter=' + response[i].someObject.id,
),
);
}
} catch (e) {
console.log(e);
}
}
function* fetchSubApi(action) {
try {
const response = yield call(get, action.endpoint);
yield put({
type: types.RECEIVE_SUB_API,
response
});
} catch (e) {
console.log(e);
}
}
takeLatest(type.GET_MAIN_API, fetchMainApi)
takeEvery(types.GET_SUB_API, fetchSubApi)
So on success of receiving sub APIs you need to insert data into your state inside reducers.
This is just pseudo code.

Related

How to call one saga at the end of another redux-saga like a async await in redux-thunk

I have two sagas, one gets the cities, the other is the weather forecast for these cities (according to ID specialists), how can I make it so that the second saga is processed at the end of the first?
method in which a call my sagas:
async componentDidMount() {
this.props.navigation.setOptions(this.navigationOptions);
//sagas call
await this.props.fetchCities();
await this.fetchForecastsHandler(this.props.userCities);
}
...some code
fetchForecastsHandler(cities: ICIty[]) {
const ids = cities.map((el) => el.id);
this.props.fetchForecasts(ids);
}``
...some code
render(){...}
My index.ts saga
export function* mySaga() {
yield takeEvery(types.FETCH_USERS_CITIES, fetchUserCitiesWorker);
yield takeEvery(types.REMOVE_CITY, removeUserCitiesWorker);
yield takeEvery(types.SEARCH_CITY, searchUserCitiesWorker);
yield takeEvery(types.FETCH_FORECAST, fetchForecastWorker);
yield takeEvery(types.ADD_NOTIFICATION, addNotificationWorker);
yield takeEvery(types.EDIT_NOTIFICATION, editNotificationWorker);
yield takeEvery(types.DELETE_NOTIFICATION, deleteNotificationsWorker);
yield takeEvery(types.FETCH_NOTIFICATION, fetchNotificationsWorker);
}
**FeychCityWorker saga:**
export function* fetchUserCitiesWorker(callback?:any) {
try {
yield put({ type: types.FETCH_USERS_CITIES_START });
//const user = yield call(Api.fetchUser, action.payload.userId);
yield delay(2000,true)
yield put({ type: types.FETCH_USERS_CITIES_SUCCESS, payload: userCities });
console.log("fetchUserCitiesWorker worked sucess")
//callback?callback():null
} catch (e) {
yield put({ type: types.FETCH_USERS_CITIES_ERROR, error: e.message });
}
}
**also storage settings just in case:**
const sagaMiddleware = createSagaMiddleware()
export const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
export const action = (type:string) => store.dispatch({type})
sagaMiddleware.run(mySaga)
You can update your componentDidMount to call only this.props.fetchCities. Update your watcher function mySaga to include
{
...,
yield takeEvery(
[FETCH_USERS_CITIES_SUCCESS],
fetchUserCitiesWorker,
)
}
This will make the payload of FETCH_USERS_CITIES_SUCCESS available in the fetchUserCitiesWorker saga
you can use fork to implement it?
example: you have type fetchUserSuccess when saga fetchUser is done. So you can run one task run on the background to check anytime fetch user done to make something.
function* waitFetchUserDone() {
while (true) {
yield take(fetchUser.SUCCESS);
// yield something
// to do something
}
}
yield fork(waitFetchUserDone);

How to make await work with redux Saga in React?

The await does not seem to work with Redux saga. I need to wait for my API call to finish and then execute the remaining code. What happens now is that AFTER CALL gets printed before the RESPONSE which means await does not seem to work at all. I'm using async calls but not sure what needs to be done extra from the redux saga side?
async componentWillMount() {
console.log("BEFORE CALL")
await this.props.getUserCredit()
console.log("AFTER CALL")
}
mapDispatchToProps = (dispatch) => {
return {
getUserCredit: () => dispatch(getUserCredit()),
}
};
connect(null, mapDispatchToProps)(MyComponent);
Action
export const getUserCredit = () => {
return {
type: GET_USER_CREDIT,
};
};
Redux Saga
const getUserCreditRequest = async () => {
const response = await Api.get(getCreditUrl)
console.log("REPONSE!!!")
console.log(response)
return response
}
function* getUserCredits() {
try {
const response = yield call(getUserCreditRequest);
if (response.status === okStatus) {
yield put({
userCredit: response.data.userCredit
}
));
}
} catch (error) {}
}
export function* getUserCredit() {
yield takeLatest(GET_USER_CREDIT, getUserCredits);
}
export default function* rootSaga() {
yield all([fork(getUserCredit)]);
}
Normally, init / fetching takes place during componentDidMount and don't use async or await inside components. Let the saga middleware do its thing via yield.
// In your component
componentDidMount() { // not async
this.props.getUserCredit(); // dispatch `GET_USER_CREDIT` action
}
mapDispatchToProps = (dispatch) => {
return {
getUserCredit: () => dispatch(getUserCredit()),
}
};
connect(null, mapDispatchToProps)(YourComponent);
You shouldn't be using async/await pattern. As redux-saga handles it by the yield keyword. By the time call is resolved you will have the value available in response.
in actions.js, you should have an action that will carry your data to your reducer:
export function getUserCredits(userCredit) {
return {
type: types.GET_USER_CREDIT_SUCCESS,
payload: userCredit
};
}
Your saga should handle the API call like so:
function* getUserCredits() {
try {
const response = yield axios.get(getCreditUrl); <--- This should work
// No need for if here, the saga will catch an error if the previous call failed
yield put(actions.getUserCredits(response.data.userCredit));
} catch (error) {
console.log(error);
}
}
EDIT: example of using axios with redux-saga

Making ajax call using redux-saga and updating store

im new to whole React env and im trying to create a GET request to Google Api using redux-saga library.
Im sort of missing 2 things. First problem is that my saga function is called again and again forever ( have no idea why ).
The second thing is how to pass the data properly to the reducer.
Here is my Saga:
function* watchAutoCompleteFetch() {
yield takeLatest(UPDATE_ZIP_AUTOCOMPLETE, requestAutoComplete);
}
function requestAutoCompleteApi() {
return fetch(
'some-url-here'
).then((response) => response.json())
.then((json) => json)
.catch((e) => {
put({type: "AUTOCOMPLETE_ZIP_FETCH_FAILED", message: e.message});
});
}
function* requestAutoComplete() {
const data = call(requestAutoCompleteApi);
yield put(updateZipAutoCompleteAction(data));
}
And reducer function:
const updateZipAutoComplete = (state, data) => {
debugger;
return state;
};
In reducer, I get the data as some sort of call object from the redux-saga, not a promise, nor the data.
Any ideas what im doing wrong?
So, turns out there was indeed 2 problems. One is that I was missing yield keyword, the second was I was calling always the same reducer, which triggered the dispatch event again and it went into loop and run forever.
The actual solutions looks like this:
function* watchAutoCompleteFetch() {
yield takeLatest(UPDATE_ZIP_AUTOCOMPLETE, requestAutoComplete);
}
function requestAutoCompleteApi() {
return fetch(
'some-url-here'
).then((response) => response.json())
.catch((e) => {
console.log(e)
});
}
function* requestAutoComplete() {
const data = yield call(requestAutoCompleteApi);
if(data.status ==="OK") {
yield put(updateZipAutoCompleteSucceedAction(data));
} else {
yield put(updateZipAutoCompleteFailedAction(data));
}
}

how to setstate after saga async request

I'm using redux-saga in my project.
I used redux-thunk before, so i can't setState ends of some async request. like
this.props.thunkAsync()
.then(){
this.setState({ '' });
}
Since thunk returns promise, i could use 'then'.
But i can't do this with saga, because saga doesn't return promise.
So i did it in componentWillReceiveProps by checking flag props (like REQUEST_SUCCESS,REQUEST_WAITING...) has been changed.
I think it's not good way to solve this problem.
So.. My question is how can i do some works when async request ends in redux-saga!
But i can't do this with saga, because saga doesn't return promise
Redux-saga is slightly different from thunk since it is process manager, not simple middleware: thunk performs reaction only on fired actions, but saga has its own "process" (Formally callback tick domain) and can manipulate with actions by effects.
Usual way to perform async actions with redux-saga is splitting original actions to ACTION_REQUEST, ACTION_SUCCESS and ACTION_FAILURE variants. Then reducer accepts only SUCCESS/FAILURE actions, and maybe REQUEST for optimistic updates.
In that case, your saga process can be like following
function* actionNameSaga(action) {
try {
const info = yield call(fetch, { params: action.params }
yield put('ACTION_NAME_SUCCESS', info)
} catch(err) {
yield put('ACTION_NAME_FAILURE', err)
}
function* rootSaga() {
yield takeEvery('ACTION_NAME', actionNameSaga)
}
Keep in mind that yield operation itself is not about promise waiting - it just delegates async waiting to saga process manager.
Every api call you make is processed as an async request but handled using a generator function in a saga.
So, After a successful api call, you can do the following possible things.
Make another api call like
function* onLogin(action) {
try {
const { userName, password } = action;
const response = yield call(LoginService.login, userName, password);
yield put(LoginService.loginSuccess(response.user.id));
const branchDetails = yield call(ProfileService.fetchBranchDetails, response.user.user_type_id);
yield put(ProfileActions.fetchBranchDetailsSuccess(branchDetails));
} catch (error) {
yield put(ProfileActions.fetchUserDetailsError(error));
}
}
Pass a Callback after successfull api
onLoginClick() {
const { userName, password } = this.state;
this.props.login(userName, password, this.onLoginSuccess);
}
onLoginSuccess(userDetails) {
this.setState({ userDetails });
}
function *onLogin(action) {
try {
const { userName, password, onLoginSuccess } = action;
const response = yield call(LoginService.login, userName, password);
if (onLoginSuccess) {
onLoginSuccess(response);
}
yield put(LoginService.loginSuccess(response.user.id));
const branchDetails = yield call(ProfileService.fetchBranchDetails,
response.user.user_type_id);
yield put(ProfileActions.fetchBranchDetailsSuccess(branchDetails));
} catch (error) {
yield put(ProfileActions.fetchUserDetailsError(error));
}
}
Update Reducer State and get from props by mapStateToProps
yield put(LoginService.loginSuccess(response.user.id));
#connect(
state => ({
usedDetails: state.user.get('usedDetails'),
})
)
static getDerivedStateFromProps(nextProps, prevState) {
const { usedDetails } = nextProps;
return {
usedDetails
}
}
I was stuck with the same problem...
My solution was wrapping the dispatch in a promise and call the resolve and reject in a saga function...
I created a hook to wrap the dispatch. You can see my example here:
https://github.com/ricardocanelas/redux-saga-promise-example
I hope that can help somebody.
You can do it this way. From component props you call the saga method and pass the function you want to execute after success or failure, like below
export function* login({ payload }) {
const url = 'localhost://login.json';
try {
const response = yield call(App_Service, { url, method: 'GET' });
if (response.result === 'ok' && response.data.body) {
yield put(fetchDataActionCreators.getLoginSuccess(response.data.body));
//function passed as param from component, pass true if success
payload.callback(true);
}
} catch (e) {
//function passed as param from component, pass false if failure
payload.callback(false);
console.log(e);
}
}
export function* watchLogin() {
while (true) {
const action = yield take(LOGIN);
yield* login(action);
}
}
export default function* () {
yield all([
fork(watchLogin)
]);
}
In component call call setState method in the function you pass to saga as param
login() {
// store
this.props.getServiceDetails({
callback:(success) => this.onLoginSuccess(success)
})
}
onLoginSuccess = (success) => {
this.setState({
login:success
})
alert('login '+success)
}

Redux saga, axios and progress event

Is there clean/short/right way to using together axios promise and uploading progress event?
Suppose I have next upload function:
function upload(payload, onProgress) {
const url = '/sources/upload';
const data = new FormData();
data.append('source', payload.file, payload.file.name);
const config = {
onUploadProgress: onProgress,
withCredentials: true
};
return axios.post(url, data, config);
}
This function returned the promise.
Also I have a saga:
function* uploadSaga(action) {
try {
const response = yield call(upload, payload, [?? anyProgressFunction ??]);
yield put({ type: UPLOADING_SUCCESS, payload: response });
} catch (err) {
yield put({ type: UPLOADING_FAIL, payload: err });
}
}
I want to receive progress events and put it by saga. Also I want to catch success (or failed) result of the axios request. Is it possible?
Thanks.
So I found the answer, thanks Mateusz BurzyƄski for the clarification.
We need use eventChannel, but a bit canningly.
Suppose we have api function for uploading file:
function upload(payload, onProgress) {
const url = '/sources/upload';
const data = new FormData();
data.append('source', payload.file, payload.file.name);
const config = {
onUploadProgress: onProgress,
withCredentials: true
};
return axios.post(url, data, config);
}
In saga we need to create eventChannel but put emit outside.
function createUploader(payload) {
let emit;
const chan = eventEmitter(emitter => {
emit = emitter;
return () => {}; // it's necessarily. event channel should
// return unsubscribe function. In our case
// it's empty function
});
const uploadPromise = upload(payload, (event) => {
if (event.loaded.total === 1) {
emit(END);
}
emit(event.loaded.total);
});
return [ uploadPromise, chan ];
}
function* watchOnProgress(chan) {
while (true) {
const data = yield take(chan);
yield put({ type: 'PROGRESS', payload: data });
}
}
function* uploadSource(action) {
const [ uploadPromise, chan ] = createUploader(action.payload);
yield fork(watchOnProgress, chan);
try {
const result = yield call(() => uploadPromise);
put({ type: 'SUCCESS', payload: result });
} catch (err) {
put({ type: 'ERROR', payload: err });
}
}
I personally found the accepted answer to be very convoluted, and I was having a hard time implementing it. Other google / SO searches all led to similar type answers. If it worked for you, great, but I found another way using an EventEmitter that I personally find much simpler.
Create an event emitter somewhere in your code:
// emitter.js
import { EventEmitter } from "eventemitter3";
export default new EventEmitter();
In your saga to make the api call, use this emitter to emit an event within the onUploadProgress callback:
// mysagas.js
import eventEmitter from '../wherever/emitter';
function upload(payload) {
// ...
const config = {
onUploadProgress: (progressEvent) = {
eventEmitter.emit(
"UPLOAD_PROGRESS",
Math.floor(100 * (progressEvent.loaded / progressEvent.total))
);
}
};
return axios.post(url, data, config);
}
Then in your component that needs this upload progress number, you can listen for this event on mount:
// ProgressComponent.jsx
import eventEmitter from '../wherever/emitter';
const ProgressComponent = () => {
const. [uploadProgress, setUploadProgress] = useState(0);
useEffect(() => {
eventEmitter.on(
"UPLOAD_PROGRESS",
percent => {
// latest percent available here, and will fire every time its updated
// do with it what you need, i.e. update local state, store state, etc
setUploadProgress(percent)
}
);
// stop listening on unmount
return function cleanup() {
eventEmitter.off("UPLOAD_PROGRESS")
}
}, [])
return <SomeLoadingBar value={percent} />
}
This worked for me as my application was already making use of a global eventEmitter for other reasons. I found this easier to implement, maybe someone else will too.

Resources