Below is a snippet of my login saga:
export function* loginFlow() {
while (true) {
const request = yield take(LOGIN_REQUEST);
const { username, password } = request.data;
const authResp = yield call(authorize, { username, password });
if (authResp) {
yield put({ type: SET_AUTH, newAuthState: true }); // User is logged in (authorized)
yield put({ type: CHANGE_FORM, newFormState: { username: '', password: '' } }); // Clear form
forwardTo('/home'); // Go to dashboard page
}
}
}
This saga is in my LoginContainer. Now everytime I go to the login screen and load the login container, a new saga "process" is spawned, so everytime I revisit the login screen, I have increasingly more and more requests going to my login API when I click the "login" button.
Can I somehow destroy the saga upon component destroy?
EDIT: Here's an attempt to cancel the saga:
export function* loginFlow() {
const request = yield take(LOGIN_REQUEST);
const { username, password } = request.data;
const authResp = yield call(authorize, { username, password });
if (authResp) {
yield put({ type: SET_AUTH, newAuthState: true }); // User is logged in (authorized)
yield put({ type: CHANGE_FORM, newFormState: { username: '', password: '' } }); // Clear form
forwardTo('/home'); // Go to dashboard page
}
}
export function* watchLogin() {
// or takeEvery (according to your business logic)
yield* takeEvery(LOGIN_REQUEST, loginFlow);
}
export function* root() {
const watchers = [
yield fork(watchLogin),
];
// Cancel all watchers on location change
yield take(LOCATION_CHANGE);
watchers.forEach(function(watcher) {
console.log("cancelling watcher")
cancel(watcher)
});
}
// All sagas to be loaded
export default [
root,
];
I have to click on the login button twice on the initial load, so that the API request is made at all, then I am experiencing the same behaviour as before - the saga doesn't get cancelled and the requests keep adding up.
Here's my component:
export class Login extends React.Component {
constructor(props) {
super(props);
this.login = this.login.bind(this);
this.onChange = this.onChange.bind(this);
}
onChange(newFormState) {
this.props.dispatch(changeForm(newFormState));
}
login(username, password) {
console.log("dispatching login request")
this.props.dispatch(loginRequest({ username, password }));
}
render() {
const { formState, currentlySending, error } = this.props;
return (
<Wrapper>
<LoginForm onChange={this.onChange} data={formState} error={error} currentlySending={currentlySending} btnText={messages.btnText} usernameText={messages.usernameText} passwordText={messages.passwordText} onSubmit={this.login} />
</Wrapper>
);
}
}
Here's how I load my sagas (routes.js):
export default function createRoutes(store) {
// create reusable async injectors using getAsyncInjectors factory
const { injectReducer, injectSagas } = getAsyncInjectors(store);
return [
{
path: '/login',
name: 'login',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/Login/reducer'),
System.import('containers/Login/sagas'),
System.import('containers/Login'),
]);
const renderRoute = loadModule(cb);
importModules.then(([reducer, sagas, component]) => {
injectReducer('login', reducer.default);
injectSagas(sagas.default);
renderRoute(component);
});
importModules.catch(errorLoading);
},
...
And here's the forwardTo function that I believe is the one causing problems:
function forwardTo(location) {
browserHistory.push(location);
}
If I break before I call this function inside the saga's while loop, the saga gets destroyed automatically and all works as expected.
Well, yes you can destroy your saga-watchers on component destruction, and that would be either by two methods:
Add to actions for component mount and unmount, then in your React component's method,
componentWillMount, dispatch the mounting action and oncomponentWillUnmountdispatch the unmounting action and handle your sagas accordingly.You'd destroy your saga-watchers on page/container NOT component destruction, and you just listen on
LOCATION_CHANGEaction (maybe fromreact-router-reduxif you use it) rather thanCOMPONENT_UNMOUNTaction (as mentioned in the first method above)Here you go a sample for applying the second method in your saga, also some modification for your
loginFlowsaga generator:Now a shown above, we use some of
redux-saga/effectsto fork saga watchers on usage of the component/container then use cancel to destroy watchers onLOCATION_CHANGE.Also, you need to dispatch you
LOGIN_REQUESTaction on buttonClick in the LoginComponent.Please, ask for clarification if something is not clear.
Read more about task cancellation from
redux-sagadocumentation here.