How to wait execution of a saga to finish in redux-saga? - reactjs

I have the following scenario:
export function* addCircle(circleApi, { payload }) {
try {
const response = yield apply(
circleApi,
circleApi.addCircle,
[payload]
);
if (response.error_type) {
yield put(addCircleFailedAction(response.error));
} else {
yield put(addCircleSucceededAction(response));
}
} catch (err) {
console.error(err);
}
}
export function* addTender(tenderApi, { payload }) {
try {
// NOTE: I want this to finish before continuing with rest of saga below.
yield call(addCircleAction(payload.circlePayload));
// Rest of saga removed for brevity.
} catch (err) {
console.error(err);
}
}
So, basically addCircle is making an API call, and depending on its success I call the appropriate redux action. Now, inside another saga I call the action responsible for addCircle saga, and I want it to finish execution before I continue with the rest of the saga. I tried to use call, but it basically doesn't wait for the addCircle saga to finish executing. Is there any way to wait for it? I call addCircle from inside my components and I didn't have the need to wait it, but in this specific instance I have to call it inside the saga, so I really need to wait for it to finish execution, change the state of the app, so that I can use the updated state in the rest of addTender saga. Any ideas?

As per your code snippet, your addCircle saga will dispatch either addCircleFailedAction or addCircleSucceededAction action creators just before it finishes execution. So we will have to wait for those action in your addTender saga.
Basically, this is what you should do. I'm just guessing your action types based on action creator names.
yield call(addCircleAction(payload.circlePayload));
yield take([ADD_CIRCLE_FAILED_ACTION, ADD_CIRCLE_SUCCEEDED_ACTION]);
// Rest of the saga
There is one edge case though. You are not dispatching any action in the catch block of your addCircle saga. Maybe you can dispatch an action called addCircleExceptionAction inside catch block and wait for it along with the other actions like this:
yield take([ADD_CIRCLE_FAILED_ACTION, ADD_CIRCLE_SUCCEEDED_ACTION, ADD_CIRCLE_EXCEPTION_ACTION]);

If you are dispatching multiple actions that would trigger addRender then there is no guarantee that take(...) would actually wait for the action that resulted of the yield call.
export function* addCircle(circleApi, { payload }) {
try {
const response = yield apply(
circleApi,
circleApi.addCircle,
[payload]
);
if (response.error_type) {
yield put(addCircleFailedAction(response.error));
return response;
} else {
yield put(addCircleSucceededAction(response));
return response;
}
} catch (err) {
console.error(err);
return {err};
}
}
export function* addTender(tenderApi, { payload }) {
try {
//because addCircle saga is returning something you can re use it
// in other sagas.
const result = yield call(addCircle,circleAPI?,payload.circlePayload);
//check for result.error_type here
// Rest of saga removed for brevity.
} catch (err) {
console.error(err);
}
}
Your code and the accepted answer would result in an error because call does not take an action object as first argument (it does take a {context,fn} type object).
Dispatching an action and then listening to another action that may or may not have been a side effect of the action you just dispatched is bad design. You dispatch these actions asynchronously and there is no guarantee they all take the same time to complete or provide the side effect you are waiting for in the same order as they were started.

Related

Redux Saga - Unable to cancel task

I have a react project set up to work with redux saga, but for some reason, I'm unable to cancel a running saga / action task. The expectancy is that, after some action, (like user navigating away, or clicking on a button), the running saga would be cancelled. Tried catching the cancelled action, but doesn't happen after the saga has run completely:
function* fetchOverviewSaga(): SagaReturnType<any> {
try {
yield delay(5000);
console.log('still running');
const response = yield call(getOverviewData);
console.log('still running');
yield all([
put(updateTagsPendingState(false)),
put(updateTagsDataState(response.items)),
]);
} finally {
if(yield cancelled()) {
console.log('saga task canceled');
}
}
}
function* cancelOverviewSaga(): SagaReturnType<any> {
const runningAction = yield fork(fetchOverviewSaga);
yield cancel(runningAction);
}
function* overviewSaga() {
yield all([
takeLatest(startOverview, fetchOverviewSaga),
takeLatest(cancelOverview, cancelOverviewSaga)
]);
}
The result is that, even after the action was dispatched for cancelling (cancelOverviewSaga), the fetchOverviewSaga still runs, do gets catched in the if(yield cancelled()) only after it completely finished running. Not sure if this is the actual behaviour, would have expected to cancel when requested. Any ideas are most welcomed.
*Edit:
Upon calling the cancel action, the fetchOverviewSaga seems to be canceled, since it does log saga task canceled, however the ones remaining to run is the block from yield all([..])), looking at the console, probably the problem lies within that block
*Edit2:
To better illustrate the behaviour:
dispatch startOverview action
immediately cancel it
the log: saga task canceled
after 5000ms (the delay finishes in fetchOverviewSaga)
the log: 'still running' x2 and dispatches the yield all from fetchOverviewSaga
The same saga can run in multiple instances independently. So e.g. if you do
const task1 = yield fork(mySaga);
const task2 = yield fork(mySaga);
task1 === task2 // false
it will run two independent instances of mySaga, each with its own task. If you cancel one, it doesn't cancel the other one. So forking and immediately canceling your fetchOverviewSaga saga in cancelOverviewSaga will have no effect on the saga that is running as a result of dispatching the startOverview action.
In this case you can instead use e.g. the race effect to achieve your goal:
function* fetchOverviewSaga() {
try {
// your fetching logic ...
} finally {
if (yield cancelled()) {
console.log("saga task canceled");
}
}
}
function* startOverviewSaga() {
yield race([
call(fetchOverviewSaga),
take(cancelOverview),
]);
}
function* overviewSaga() {
yield takeLatest(startOverview, startOverviewSaga);
}
The race effect waits for one if the items in the array to finish and then it cancels all other ones, and so:
If the cancelOverview action is disptached before the fetchOverviewSaga is finished it will cancel the fetchOverviewSaga
If the fetchOverviewSaga finishes before a cancel action is dispatched it will just stop waiting for the cancel action.
Working demo:
https://codesandbox.io/s/https-stackoverflow-com-questions-69717869-redux-saga-unable-to-cancel-task-vu4b0?file=/src/index.js
When you cancel a saga, all sub tasks cancel. Example:
function* mainTask(){
const task = yield fork(subTask1) // or call
yield cancel(task)
}
function* subTask1(){
yield fork(subTask2) // or call
}
function* subTask2(){
...
}
In this example, since cancel propagates downward, subTask1 will be cancelled through mainTask and subTask2 will be cancelled through subTask1.
However, when you yield put, you dispatched an action, which is caught in a reducer or listened in a saga. This is just like sending an erroneous e-mail, and sending a second e-mail to fix the error. Therefore, you can dispatch another action inside if (yield cancelled()) to undo what other actions do.
One way
} finally {
if(yield cancelled()) {
yield all[
put(cancelUpdateTagsPendingState()),
put(cancelUpdateTagsDataState()),
]
}
}
Helpful docs

Confusion with dispatching an action in loop and tracking progress individually

I am dispatching an action let's say "GET_STATUS" in a loop for X number of time from a component.
In saga file I have
function* actionWatcher() {
yield all([
takeLatest(Actions.GET_LATEST, getLatest),
]);
}
Inside getLatest* function there is this API call
//Some code
const results = yield call(api, {params});
//code after
callback()
I can clearly see API being called X number of time in network and also in chrome debugger I can see //Some code is executed X number of time. But //code after is executed only once in the end and callback function is being called just once in the end.
I am expecting to be called for each occurrence.
If multiple Actions.GET_LATEST happen in rapid succession, then takeLatest is designed to cancel the old saga, and start a new one. If the saga is canceled while it's executing const results = yield call(api, {params});, that means it will never get to callback()
If you don't want them to be canceled, then use takeEvery instead of takeLatest
function* actionWatcher() {
yield all([
takeEvery(Actions.GET_LATEST, getLatest),
]);
}
If you want to keep the cancellation, but you need the callback to be called even if it's cancelled, you can use a try/finally:
function* getLatest() {
try {
const results = yield call(api, {params});
} finally {
// This code will run whether it completes successfully, or throws an error, or is cancelled
callback();
}
}
If you need to specifically check whether it was cancelled in order to perform custom logic, you can do so with an if (yield cancelled()) in the finally block:
function* getLatest() {
try {
const results = yield call(api, {params});
callback(); // This line will only run if it's not cancelled and does not throw
} finally {
if (yield cancelled()) {
// This line will only run if cancelled
}
}
}

the same action when dispatched for the second time does not call the saga, while it does correctly in the first tie

When the saga is being called on an action after the first load it is working correctly.
but when the same action is performed twice on the saga the action is dispatched but the saga never receives it.
useEffect(() => {
if (show) {
updateTitle(message.title);
if (!status || status === Status.Initiate) {
createStatus();
}
}
return () => {
if (!show) {
cleanup();
}
};
}, [status]);
The below process solved my issue.
Turns out, in the runtime, if some saga fails to execute, that saga is never called again, that means if any action is generated for that saga it is just ignored.
To check if the saga is failing during the runtime, you can wrap the body of saga inside a try catch block.
function* failingSaga() {
try {
// saga-body
} catch(e) {
console.log(e);
}
}
this will help in getting to know what the exception is.

Call two different action types from one redux-saga

I've been able to get by with basic sagas implementation for now but my app is getting a little more complex. I chose sagas for the asynchronous capabilities but seem to have misunderstood how things work.
I have a global search input within my application that needs to make two different api calls (different data objects), but the search input also has it's own loading states based on the search/ status of api calls. Based on this information this is the flow of the application:
Search happens (dispatches the action GLOBAL_SEARCH_REQUEST)
The saga watcher for GLOBAL_SEARCH_REQUEST kicks off (sets loading to true for the input)
In that saga - make a call to get all users / subscriptions that match the search query
On success, set loading for the input to false
On failure, set error
the global search request saga
function* globalSearchRequestSaga(action) {
const { query } = action
console.log(`searching subscriptions and users for : ${query}`)
try {
yield put(fetchUsersRequest(query))
// call for the subscriptions (leaving it out for simplicity in this example)
yield put(globalSearchSuccess(query))
} catch (error) {
console.log(`error: ${error}`)
yield put(globalSearchFailure(error.message))
}
}
where the fetch users saga looks like
export function* fetchUsersRequestSaga(action) {
const { query } = action
const path = `${root}/users`
try {
const users = yield axios.get(path, { crossDomain: true })
yield put(fetchUsersSuccess(query, users.data))
} catch (error) {
console.log(`error : ${error}`)
yield put(fetchUsersFailure(query, error.message))
}
}
(very basic)
If I do things this way, there is an issue where the the GLOBAL_SEARCH_SUCCESS action is executed before the completion of the request for users ( and I imagine the same thing if I added in subscriptions api call as well). One solution I found is if I change the line
yield put(fetchUsersRequest(query))
to
yield call(fetchUsersRequestSaga, fetchUsersRequest(query))
where fetchUsersRequestSaga is the saga from above, and fetchUsersRequest(query) is the action creator for fetching users. This causes the asnyc functionality to work, and GLOBAL_SEARCH_SUCCESS waits for the return of the users (correct behavior).
The only issue with this is that the FETCH_USERS_REQUEST action is no longer logged to the store.
I am wondering if there is a way to either get this to properly log to the store, or return to my previous implementation with proper blocking on the put(fetchUsersRequest(query))
The put function is a non-blocking action. It won't wait till the promise/api request resolves.
I would suggest you to just call sagas directly instead of dispatching actions.
try {
yield call(fetchUsersRequestSaga, query);
yield call(globalSearchSaga, query); // or whatever its called
}
call is a blocking action. It will wait until the request finishes, so both if your calls will execute in proper order.
It's been a while since I worked with sagas but here is some code that will give you a general idea how to wait for a dispatched action.
The way it works is that when you fetch and want to wait for it to fail or succeed you give the fetch action an id, then you can pass that to the waitFor function while simultaneously dispatch the action.
If you don't want or need to wait for it then you can just dispatch the action without an id and it'll still work:
const addId = (id => fn => (...args) => ({
...fn(...args),
id: id++,
}))(0);
const withId = ({ id }, action) => ({ action, id });
function* waitFor(id) {
const action = yield take('*');
if (action.id === id) {
return action;
}
return waitFor(id);
}
function* globalSearchRequestSaga(action) {
const { query } = action;
console.log(
`searching subscriptions and users for : ${query}`
);
try {
//add id to action (id is unique)
const action = addId(fetchUsersRequest, query);
//dispatch the action and then wait for resulting action
// with the same id
yield put(action);
const result = yield waitFor(action.id);
// call for the subscriptions (leaving it out for simplicity in this example)
yield put(globalSearchSuccess(query));
} catch (error) {
console.log(`error: ${error}`);
yield put(globalSearchFailure(error.message));
}
}
export function* fetchUsersRequestSaga(action) {
const { query } = action;
const path = `${root}/users`;
try {
const users = yield axios.get(path, {
crossDomain: true,
});
yield put(//add original id to success action
withId(action, fetchUsersSuccess(query, users.data))
);
} catch (error) {
console.log(`error : ${error}`);
yield put(
withId(//add original id to fail action
action,
fetchUsersFailure(query, error.message)
)
);
}
}

Is it idiomatic use of redux-saga to call a worker saga directly from a React component?

I am using redux-saga having spent some time on core concepts of generators, generators with promises, and redux-saga itself. What I want below is to understand what is idiomatic and recommended, and what isn't.
In one file I define my root saga, watcher saga, and one worker saga (fetchSupplierOrders).
import {
fetchSupplierOrders,
} from './supplierOrders';
import { takeLatest} from 'redux-saga/effects';
function* watchSupplierOrdersSagas() {
yield takeLatest('REQUEST_FETCH_SUPPLIER_ORDERS', fetchSupplierOrders);
}
export default function* rootSaga() {
yield all([watchSupplierOrdersSagas()]);
}
Here is the worker saga:
export function* fetchSupplierOrders() {
try {
const supplierOrders = yield call(getSupplierOrders); // API call is in getSupplierOrders
// normally here I would use redux-saga put to hit my redux reducers
yield supplierOrders.map(({ id }) => id)
} catch (error) {
yield put({ type: 'RECEIVE_ERROR_FETCH_SUPPLIER_ORDERS', error: error.message });
}
}
I have a React component that when I click a button, it executes the worker saga. What I am trying to do here is to not go through the redux-saga watcher saga at all. I will simply execute the generator function myself in the component, and iterate through it. Usually, I would go through the watcher saga, and it would call a worker saga that would generate side effects by modifying redux state.
However, what if I want to make a network request, but I don't want to save the result in redux, but in local component state? I want the component to somehow get the results from the worker saga directly.
Here is the click handler for the button in the React component:
const handleFetchSuppliers = event => {
const it = fetchSupplierOrders({ payload: event.target.value });
const res1 = await it.next().value;
console.log('res1', res1);
const res2 = it.next(res1);
console.log('res2', res2);
This code will not work, because in the worker saga I am using redux-saga's call function. If I remove the use of call, and call getSupplierOrders (an async function) directly, then the await works and all the correct values are console.logged.
Is it common to do this (executing a worker saga from a component to get the results of an API request)? But if I do it this way then I lose the benefit of using call (isn't this useful because it's easier to test?)
Before redux-saga I would simply dispatch a thunk using redux-thunk, which is basically using async/await all the way through.
Do people mix the use of redux-thunk and redux-saga? Is this frowned upon?
However, what if I want to make a network request, but I don't want to save the result in redux, but in local component state?
If redux is not involved, then redux-saga is not the right tool to use. Just use the normal react approach: make the api request (often in componentDidMount), then wait for that promise to complete (with .then or await), then call setState.
If you want to have multiple ways to do the fetch (both via a saga, and via a direct call), then you could put the fetch into a helper function (regular function, not generator). The component and the saga could then both make use of the helper function, each wrapping it with whatever extra work they need to do.
For example:
// helper
async function fetchStuff() {
const response = await fetch('some Url');
if (!response.ok) {
throw response.status;
}
const data = await response.json();
return data.map(({ id }) => id);
}
// In a saga...
function* watchFetch() {
yield takeLatest('doFetch', fetchStuffSaga);
}
function* fetchStuffSaga() {
try {
const data = yield call(fetchStuff);
yield put({ type: 'success', payload: data });
} catch (err) {
yield put({ type: 'error', payload: err });
}
}
// In a component that dispatches an action:
componentDidMount() {
this.props.dispatch({ type: 'doFetch' });
}
// In a component that doesn't want to dispatch an action:
async componentDidMount() {
try {
const data = await fetchStuff();
this.setState({ data });
} catch (err) {
this.setState({ error: err });
}
}
This code will not work, because in the worker saga I am using redux-saga's call function. If I remove the use of call, and call getSupplierOrders (an async function) directly, then the await works and all the correct values are console.logged.
Sagas are not meant for manual iteration. If you try to manually iterate through a saga, you either need to have specialized knowledge about exactly what the saga will yield in what order, or you basically need to re-implement redux-saga yourself. The former is brittle and tightly coupled, the latter is a waste of effort.
Is it common to do this (executing a worker saga from a component to get the results of an API request)?
No.
Do people mix the use of redux-thunk and redux-saga? Is this frowned upon?
They're both trying to handle the same kinds of things (asynchronous actions). Your codebase will be simpler if you use just one approach, then trying to mix and match both.

Resources