react-boilerplate: SAGA: Cannot read property cancel of undefined

React Boilerplate

Issue Type

Description

Hi there,

Just wanted to point out that the following issue has not been resolved yet: https://github.com/react-boilerplate/react-boilerplate/issues/2021

@kai23 solution of adding ‘&& descriptor.task’ on line 75 worked for me.

export function ejectSagaFactory(store, isValid) {
  return function ejectSaga(key) {
    if (!isValid) checkStore(store);

    checkKey(key);

    if (Reflect.has(store.injectedSagas, key)) {
      const descriptor = store.injectedSagas[key];
      if (descriptor.mode && descriptor.mode !== DAEMON && descriptor.task) {
        descriptor.task.cancel();
        // Clean up in production; in development we need `descriptor.saga` for hot reloading
        if (process.env.NODE_ENV === 'production') {
          // Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga`
          store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign
        }
      }
    }
  };
}

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 4
  • Comments: 19 (6 by maintainers)

Most upvoted comments

i did the following in injectSaga

import React from 'react';
import PropTypes from 'prop-types';
import hoistNonReactStatics from 'hoist-non-react-statics';

import getInjectors from './sagaInjectors';

export default ({
  key, saga, mode, args,
}) => (WrappedComponent) => {
  class InjectSaga extends React.Component {
    static WrappedComponent = WrappedComponent;

    // eslint-disable-next-line react/destructuring-assignment
    injectors = getInjectors(this.context.store);

    static displayName = `withSaga(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`;

    static contextTypes = {
      store: PropTypes.object.isRequired,
    };

    componentWillMount() {
      const { injectSaga, ejectSaga } = this.injectors;
      const injectedArgs = args || [this.props];
      const { store } = this.context;

      // eject old saga with same name, so it does not get double sagas injected
      if (
        Reflect.has(store.injectedSagas, key)
        && store.injectedSagas[key].saga === saga
      ) {
        ejectSaga(key);
      }

      injectSaga(key, { saga, mode }, ...injectedArgs);
    }

    componentWillUnmount() {
      const { ejectSaga } = this.injectors;
      const { store } = this.context;

      if (
        Reflect.has(store.injectedSagas, key)
        && store.injectedSagas[key].saga === saga
      ) {
        ejectSaga(key);
      }
    }


    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return hoistNonReactStatics(InjectSaga, WrappedComponent);
};

never have duplicated injected sagas this way

I ran into similar issue, and this was because of race condition between injecting saga and ejecting them. componentWillUnMount is used for ejecting the saga which is a asyncrounous in React 16. Because of this you may inject a new instance of saga to the same key making the previous saga unaccessible. This should be fixed by keeping sagas mode DAEMON by default and should be fixed with #2430

Note: I don’t have a suggested fix, but hopefully the below details can help the next dev better understand the issue.

Update: It looks like this comment details out the below in a more clear way.

I too have this issue in production mode. The issue occurs when I switch languages using react-intl. Some of my components use InjectIntl directly in order to translate strings (rather than HTML elements). See API docs for more details.

The root of the problem lies within the React component lifecycle.

InjectIntl will wrap my component. injectSaga also wraps my component. Since the ejectSaga logic is run on componentWillUnmount, the logic is asynchronous. In my particular case, changing the language within my app triggers an update down to my component that is wrapped in both InjectIntl and injectSaga.

The resulting order of execution is as follows:

  1. App loads
  2. Wrapped component mounts
  3. Saga is injected with the should-be unique key

Debugging output at this point is as follows:

componentWillMount homePage InjectIntl(n)
{
    homePage: {saga: ƒ, mode: "@@saga-injector/restart-on-remount", task: {…}}
}
  1. User interaction causes dispatched action to update the selected language/locale
  2. The updated InjectIntl wrapped component mount logic applies FIRST (which re-injects the saga)

Debugging output for this action is as follows:

componentWillMount homePage InjectIntl(n)
{
    homePage: {saga: ƒ, mode: "@@saga-injector/restart-on-remount", task: {…}}
}

At this point, you may be able to see the obvious problem… the saga was already injected (and had not yet ejected), so re-injecting the already existing saga does nothing.

  1. The older InjectIntl wrapped component unmount logic applies SECOND (which ejects the saga)

Debugging output for this action is as follows:

componentWillUnmount homePage InjectIntl(n)
{
    homePage: "done"
}

From this point , any attempt to run the saga will result in the following exception:

uncaught at Ne TypeError: Cannot read property 'cancel' of undefined

Update: After reading #2021, I realized the update order is most likely impacted by React Router. Rearranging the connected components in app.js to wrap the LanguageProvider with the ConnectedRouter allows me to trigger a locale update without remounting the router-connected component.

const render = (messages) => {
  ReactDOM.render(
    <Provider store={store}>
      <ConnectedRouter history={history}>
        <LanguageProvider messages={messages}>
          <App />
        </LanguageProvider>
      </ConnectedRouter>
    </Provider>,
    MOUNT_NODE
  );
};

Closing for lack of activity + the APIs have changed so much I’m not sure these issues are still relevant. If you need to report new bugs with reducer or saga injection, please open an issue in our sister project redux-injectors. Thanks 🙏

@tatemz I just face similar scenario with your few day ago. I elaborate here maybe could be someone ref.

In my case, when container/App/index.js mounted, componentDidMount will dispatch(initApp()) action to fetch userConfig. And we set language locale According userConfig, and things happen

According to my understanding:

  1. saga A(1) is injected for mount Page A(1)
  2. dispatch(initApp())
  3. set language locale
  4. <IntlProvider /> re-mount (due to react-boilerplate design locale as element's key)
  5. Page A remount

(and due to React 16 life change)

  1. saga A(2) is injected for Page A(2) WillMount
  2. saga A(1) is ejected for Page A(1) WillUnMount

at this point, saga A(2) still work perfectly.

and We find out things broken by following scenario

  1. push to Page B (saga A(2) didn’t cancel, cus redux-saga think it not running. without any console.error)
  2. push to Page A then saga A(3) injected
  3. then every dispatch(action()) will make trigger twice

react-intl

further explain, In my view, I don’t think this design is a bug

react-boilerplate design locale as element's key

redux-saga

// /node_modules/redux-saga/es/internal/proc.js

  function cancel() {
    // at step 8
   console.log(iterator._isRunning) // false
   console.log(!iterator._isCancelled) // false
    if (iterator._isRunning && !iterator._isCancelled) {
      iterator._isCancelled = true;
      taskQueue.cancelAll();

      end(TASK_CANCEL);
    }
  }

Steps to reproduce

demo

FYI