Redux-saga calls action after cancel - reactjs

I'm trying to implement React-boilerplate with redux-saga inside. So i'm trying to fetch some data from the server and then make a redirect to another page. The problem is that before redirecting saga makes second request to the server. I guess there is something wrong with cancelling it. Here is a part of my code:
export function* fetchData() {
...
console.log('fetched');
yield browserHistory.push('/another-page');
}
export function* fetchDataWatcher() {
while (yield take('FETCH_DATA')) {
yield call(fetchData);
}
}
export function* fetchDataRootSaga() {
const fetchWatcher = yield fork(fetchDataWatcher);
yield take(LOCATION_CHANGE);
yield cancel(fetchWatcher);
}
export default [
fetchDataRootSaga
]
So in this example i have two console logs, the second one appears before redirecting. How can i fix it?
And another question. Actually, i have more functions in this file. Should i create "rootSaga" for each of them or i can cancel them all in that fetchDataRootSaga()? I mean is it normal if i cancel sagas this way:
export function* fetchDataRootSaga() {
const watcherOne = yield fork(fetchDataOne);
const watcherTwo = yield fork(fetchDataTwo);
...
yield take(LOCATION_CHANGE);
yield cancel(watcherOne);
yield cancel(watcherTwo);
...
}
Thanks in advance!
P.S. I'm not sure if this code is best practices. It is inspired by this repository

Maybe start by adjusting your loop inside fetchDataWatcher to look a little more like this
export function* fetchDataWatcher() {
while (true) {
yield take('FETCH_DATA');
yield call(fetchData);
}
}
Also you can route better by doing something like this perhaps
import { push } from 'react-router-redux';
import { put } from 'redux-saga/effects';
export function* fetchData() {
...
console.log('fetched');
yield put(push('/another-page'));
}
Overall I would hesitate to put a route change and then altogether separately do a take on it, only if you wish to cancel on all location changes (but I assume that's what you're after :) )

This defeats the purpose of saga, which is to handle potentially long running async requests and returns. You could instead set a state in your redux store like so
export function* fetchData() {
...
console.log('fetched');
yield put(setRedirectState('/another-page'));
}
Then see if the redirect state is set in your container in ComponentWillUpdate and redirect accordingly to something like this
import { push } from 'react-router-redux';
dispatch(push(state.redirecturl))
I haven't tried this, but the experience I have with React-boilerplate, this is what I would try first.

Related

Why takeLeading redux-sagas isn't working

I'm not quite sure why takeLeading isn't working for me (takeLeading is supposed to take the first call and ignore subsequent calls until the first is returned). It's calling the same call 3 separate times like a takeEvery with the same parameters from 3 separate components in their useEffect(() => {getApiWatcher(params)}, []) on mount hook. It appears those don't return before the second is called either so I know it's not 3 uniquely separate calls.
function getApi(params) {
console.log('GET CALL') // called 3 times in console and network tab
return Api.doCall(
`API/${params.number}/${params.type}`,
'GET'
);
}
function* getApiActionEffect(action) {
const { payload } = action;
try {
const response = yield call(getApi, payload);
yield put(getApiSuccess({ data: response.data, status: response.status }));
} catch (e) {
yield put(getApiError(e.response));
}
}
export function* getApiActionWatcher() {
yield takeLeading( // should make sure only first call is made and subsequent are ignored
GET_API_WATCHER,
getApiActionEffect
);
}
// action
export function getApiWatcher(payload) {
return { type: GET_API_WATCHER, payload };
}
// passed dispatch as props
const mapDispatchToProps = (dispatch) => bindActionCreators( { getApiWatcher, }, dispatch );
// root saga
export default function* rootSaga() {
yield all([... getApiActionWatcher(),...])
}
There is a lot more code involved so I'm not creating a sample jsfiddle, but ideas for what could potentially be going wrong are what I'm looking for! Might have over looked something.
Turns out there were duplicate imported functions in the root saga. For example:
// root saga
export default function* rootSaga() {
yield all([
... getApiActionWatcher(),...
... getApiActionWatcher(),...
])
}
Removing the duplicates solved the issue. It also removed other duplicate calls I wasn't working on.
If you want to handle every GET_API_WATCHER action then you better takeEvery redux-saga helper.
And docs says that task spawned with takeLeading blocks others tasks until it's done.
takeLeading is working as intended. The calls to getApi instantly returns.
The only way your code would work the way you want it to is if getApi returns a Promise. If getApi() were to return a Promise, the getApiActionEffect would block until the Promise resolved.

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 how to stop action from further propagating

I'm new to Redux Saga. I want to stop an action from further propagating. I am implementing row-level auto-saving mechanism. I use saga to detect row switch action, and then submit row changes and insert current row change action. codes like this:
// action-types.js
export const
SWITCH_ROW='SW_ROW',
CHANGE_CUR_ROW='CHG_CUR_ROW';
// actions.js
import {SWITCH_ROW,CHANGE_CUR_ROW} from './action-types'
export const switchRow=(oldRow,newRow)=>({type:SWITCH_ROW,oldRow,newRow})
export const changeRow=(row)=>({type:CHANGE_CUR_ROW,row})
// component.js
class MyComponent extends Component{
switchRow=(row)=>{
var oldRow=this.props.curRow;
this.props.dispatc(switchRow(oldRow,row));
}
render(){
...
{/* click on row */}
<div onClick={()=>this.switchRow(row)}>...</div>
...
}
}
// sagas.js
import {SWITCH_ROW} from './action-types'
import {changeRow} from './actions'
function* switchRow({oldRow,newRow}){
// Here I want to stop propagating SWITCH_ROW action further
// because this action is only designed to give saga a intervention
// point but not to be handled in reducer. I want a statement like
// below:
// stopPropogate();
if(oldRow && oldRow.modified===true){
yield call(svc.submit, oldRow);
}
yield put(changeRow(newRow))
}
export default function*(){
yield takeEvery(SWITCH_ROW,switchRow)
}
I know I can just ignore the SWITCH_ROW action in reducer. But, I think it's better if there is as least round trip as possible in program. Any suggests?
After more reading about Redux middle-ware, I think it's better to use a middle-ware to approach this goal.
At first, I renamed all type names of special actions for saga making them all starting with SAGA_. And then I use a Redux middle-ware to identify them and swallow them, and then those special actions can't reach reducer any more. Here is the codes:
// glutton.js
const glutton = () => next => action => {
if (!action.type.startsWith('SAGA_')) return next(action);
}
// store.js
...
const store = createStore(rootReducer, applyMiddleware(sagaMiddleWare, glutton, logger));
Lets say you are in a file which is not redux component, i mean you don't have access to dispatch , so i think throwing an error would be nice, and here is the code to catch the exception anywhere:
export default function autoRestart(generator) {
return function* autoRestarting(...args) {
while (true) {
try {
yield call(generator, ...args);
} catch (e) {
console.log(e);
yield put({ type: ReduxStates.Error });
}
}
}
};
all your sagas, need to implement this base function.

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