How to cancel running saga on action dispatch, while using takeEvery - reactjs

I need to run a saga every time an action is performed - but also cancel it when a logout action is dispatched. I tried using the while pattern described in the documentation, but this is only firing on the first action dispatch (but it's cancelling as expected).
export default function* rootSaga() {
while (yield take(actionTypes.sagas.RUN_COURSES_SAGA)) {
const syncTask = yield fork(processCourseLibraryFiles)
yield take(actionTypes.auth.LOGOUT_USER)
yield cancel(syncTask)
}
}
I tried replacing this is takeEvery, which fixes the problem of it only firing once - but now I'm stuck on how to cancel each of these sagas on logout action.
export default function* rootSaga() {
yield takeEvery('actionName', fn)
// some way to cancel these sagas on action
}
Appreciate any insights!

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

How do I run a Redux Saga one time to dispatch an action on start up of React App?

I am looking to dispatch an action on the start-up of my application, currently, I am using a saga to try to do the dispatching. The intent is to fetch the config from an API in the background once the application starts but for now, I am setting my domain with environment variables.
I wanted this action to be dispatched once, and detached from a specific react component.
domainSaga.js
import { put, takeEvery } from 'redux-saga/effects';
import { DOMAIN_SET_ACTION, setDomain } from './domainAction';
function* fetchDomain() {
yield put(
setDomain(
process.env.REACT_APP_API_DOMAIN_CODE,
process.env.REACT_APP_API_DOMAIN_SERVER
)
);
}
function* domainSaga() {
yield takeEvery(DOMAIN_SET_ACTION, fetchDomain);
yield put(DOMAIN_SET_ACTION); // Error cant dispatch from a saga
// dispatch set action once and only once here
// fetchDomain(); calling this runs infinitely and crashes the app
}
export default domainSaga;
Is it even possible to have this kind of dispatching with redux saga?

How to get the new state with Redux-Saga?

I'm trying to get the new state to which the getCart() generator function returns me in reducer, but the state is coming "late".
The state I need comes only after the second click of the button.
NOTE: The error on the console I am forcing is an action.
import { call, put, select, all, takeLatest } from 'redux-saga/effects';
import { TYPES } from './reducer';
import { getCart, getCartSuccess, getCartFail } from './actions';
import API from 'services/JsonServerAPI';
export function* getCartList() {
try {
const response = yield call(API.get, '/2cart');
yield put(getCartSuccess(response.data));
} catch (error) {
yield put(
getCartFail(error.response ? error.response.statusText : error.message)
);
}
}
export function* addToCart({ id }) {
yield put(getCart());
yield select(({ CartReducer }) => {
console.log(CartReducer);
});
console.log(id);
}
// prettier-ignore
export default all([
takeLatest(TYPES.GET, getCartList),
takeLatest(TYPES.ADD, addToCart)
]);
Since getCartList performs async actions you will need some way to wait for those to complete in the addToCart before logging.
One option is to call the getCartList directly from the addToCart saga without dispatching a redux action - this may not be preferable if you have other middleware that relies on TYPES.GET being dispatched.
export function* addToCart({ id }) {
// call the `getCartList` saga directly and wait for it to finish before continuing
yield call(getCartList);
yield select(({ CartReducer }) => {
console.log(CartReducer);
});
console.log(id);
}
The other option is take on the list of actions that will be dispatched once the getCartList saga completes:
export function* addToCart({ id }) {
yield put(getCart());
// wait until one of the success or failure action is dispatched, sub with the proper types
yield take([TYPES.GET_SUCCESS, TYPES.GET_FAILURE]);
yield select(({ CartReducer }) => {
console.log(CartReducer);
});
console.log(id);
}
This has some potential tradeoffs as well - you will need to make sure the action list in take stays up to date with all possible ending types that getCartList can put and you need to make sure you keep using takeLatest (vs say takeEvery) to trigger addToCart so you don't end up with multiple concurrent sagas that could fulfill the take clause.

redux-saga - Server-Side rendering with 1 async dependent on another

I am trying to get my app to Server-Side render via the END effect (details on https://github.com/redux-saga/redux-saga/issues/255, with explanations why this is so tricky).
My data relies on 2 async requests: getJwtToken -> (with token data) FetchItem -> now render.
Is this possible at all?
I have spent time looking at Channels (here https://redux-saga.js.org/docs/advanced/Channels.html) and could not get any variation to work.
My Saga looks something like this (LOAD_USER_PAGE is fired initially)
function* loadUserPage() {
yield put({type: 'JWT_REQUEST'})
const { response } = yield call(fetchJwtToken)
if (response) {
yield put({type: 'JWT_REQUEST_SUCCESS', payload: response})
}
}
function* fetchItem() {
console.log('NEVER GETS HERE')
}
function* watchLoadPage() {
yield takeLatest('LOAD_USER_PAGE', loadUserPage);
}
function* watchFetchItem() {
yield takeLatest('JWT_REQUEST_SUCCESS', fetchItem);
}
export default function* rootSaga() {
yield all([
fork(watchLoadPage)
fork(watchFetchItem)
])
}
I believe I understand why it doesn't work (due to END event fired terminating only those effects which have started, and since my 2nd effect will not be fired until my first is back, it is not included in runSaga().done promise.
By doesn't work I mean the action JWT_REQUEST_SUCCESS is fired and the runSaga.done promise runs. But my message in console.log is not fired.
I think its possible by having both requests in the same function, but I am trying to abstract the token auth part out.
Is it not possible in any way?
Really stuck.
EDIT:
The solution was to use channels as suggested in the comment on https://github.com/redux-saga/redux-saga/issues/255#issuecomment-323747994 and https://github.com/redux-saga/redux-saga/issues/255#issuecomment-334231073.
SSR for all :)

Correct way to throttle HTTP calls based on state in redux and react

What I need:
A controlled component that is an input[type="search"]. After say, 1 second of no changes to that component I want to send out an HTTP call to execute that search and have the results shown in another component.
How I've done it:
Whenever I call dispatch(setSearch(str)); I make a call to dispatch(displaySearch(false)); and then _.throttle a call to dispatch(displaySearch(true));
It feels to me that doing this sort of work in the component is incorrect, but I can't think of a way to do this in a reducer in redux.
You have different options to solve this.
1. Debounce your action at a component level
This is the simplest approach. When the input triggers a change, it calls
a debounced version of setSearch delaying the server call.
import * as React from "react"
import {connect} from "react-redux"
import {setSearch} from "./actions"
export default connect(
null,
function mapDispatchToProps(dispatch) {
const setSearch_ = _.debounce(q => dispatch(setSearch(q)), 1000)
return () => ({setSearch: setSearch_})
}
)(
function SearchForm(props) {
const {setSearch} = props
return (
<input type="search" onChange={setSearch} />
)
}
)
2. Debounce using redux-saga
This approach requires more boilerplate but gives you a lot more control over
the workflow. Using a saga we intercept the SET_SEARCH action, debounce it,
call the API then dispatch a new action containing the results.
import {call, cancel, fork, put, take} from "redux-saga/effects"
import {setSearchResults} from "./actions"
import {api} from "./services"
import {delay} from "./utils"
export default function* searchSaga() {
yield [
// Start a watcher to handle search workflow
fork(watchSearch)
]
}
function* watchSearch() {
let task
// Start a worker listening for `SET_SEARCH` actions.
while (true) {
// Read the query from the action
const {q} = yield take("SET_SEARCH")
// If there is any pending search task then cancel it
if (task) {
yield cancel(task)
}
// Create a worker to proceed search
task = yield fork(handleSearch, q)
}
}
function* handleSearch(q) {
// Debounce by 1s. This will lock the process for one second before
// performing its logic. Since the process is blocked, it can be cancelled
// by `watchSearch` if there are any other actions.
yield call(delay, 1000)
// This is basically `api.doSearch(q)`. The call should return a `Promise`
// that will resolve the server response.
const results = yield call(api.doSearch, q)
// Dispatch an action to notify the UI
yield put(setSearchResults(results))
}
Associate delay and takeLatest:
import { all, takeLatest, call } from 'redux-saga/effects';
import { delay } from 'redux-saga';
function* onSearch(action) {
yield call(delay, 1000); // blocks until a new action is
// triggered (takeLatest) or until
// the delay of 1s is over
const results = yield call(myFunction);
}
function* searchSagas() {
yield all([
// ...
takeLatest(ON_SEARCH, onSearch),
]);
}
export default [searchSagas];

Resources