Skip to content

Unsubscribe automatically $onDestroy #194

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
nico1510 opened this issue Mar 6, 2018 · 2 comments
Open

Unsubscribe automatically $onDestroy #194

nico1510 opened this issue Mar 6, 2018 · 2 comments

Comments

@nico1510
Copy link

nico1510 commented Mar 6, 2018

Just a quick question/RFC:

If you look at the react-redux source code you see that they unsubscribe from the store when the component is unmounted (https://github.com/reactjs/react-redux/blob/master/src/components/connectAdvanced.js#L168). The user doesn't have to remember to always include the unsubscribe when the component is destroyed. Would that be possible for angularjs too ? Like adding something like the following:

target.$onDestory = (() => {
  unsubscribe();

  target.$onDestory();
})(); 

to https://github.com/angular-redux/ng-redux/blob/master/src/components/connector.js#L58 ?

@AntJanus
Copy link
Collaborator

Automatic unsubscribe isn't possible but AngularJS does have a component/scope lifecycle that you can hook into via $scope.$on('$destroy', () => {})

if you're using AngularJS components, you can add a method called $onDestroy which will run when component is unmounted.

So why can't this be done automatically? because a user doesn't have to connect to the actual component!

You can bind to $scope (which has the $on method), you can bind to the component (via this), or you can bind to a random object if you want to.

@AprilArcus
Copy link
Contributor

AprilArcus commented May 6, 2022

If you are targeting browsers that support FinalizationRegistry, you can decorate $ngRedux like so:

import ngRedux from 'angular-redux';
angular.module(ngRedux)
  .decorator('$ngRedux', ['$delegate', $ngRedux => {
      const registry = new FinalizationRegistry(unsubscribe => {
        unsubscribe();
       });
       const connect = $ngRedux.connect;
       $ngRedux.connect = function connect (...connectArgs) {
         const connector = connect.apply(this, connectArgs);
         return (...connectorArgs) => {
           const unsubscribe = connector(...connectorArgs);
           const [target] = connectorArgs;
           registry.register(target, unsubscribe);
           return unsubscribe;
         };
       };
       return $ngRedux;
  }]);

if not, you can try this eldritch horror (requires Map from es6, or a polyfill):

angular.module('ngRedux')
  .decorator(
    '$ngRedux',
    [
      '$delegate',
      '$rootScope',
      '$log',
      function ($ngRedux, $rootScope, $log) {

        var targets = new Map()
          , Scope = $rootScope.constructor
          , connect = $ngRedux.connect;

        function unsubscribeAll (target) {

          var targetInfo = targets.get(target)
            , subscriptions = targetInfo.subscriptions
            , destructorDescriptor = targetInfo.destructorDescriptor
            , i
            , unsubscribe;

          for (i = 0; i < subscriptions.length; i++) {
            unsubscribe = subscriptions[i];
            unsubscribe();
          }

          // attempt to leave the target in pristine condition, in case
          // the target's lifetime is not actually managed by AngularJS.
          if (destructorDescriptor) {
            Object.defineProperty(target, '$onDestroy', destructorDescriptor);
          } else {
            delete target.$onDestroy;
          }
          targets.delete(target);
        }

        $ngRedux.connect = function connect () {

          var connector = connect.apply(this, arguments);

          return function unsubscribe () {

            var target = arguments[0]
              , unsubscribe = connector.apply(undefined, arguments)
              , destructorDescriptor
              , superDestructor;


            if (target instanceof Scope) {

              // This is the simple case. If the target is a Scope, we can
              // individually register each subscription with the Scope's
              // own lifecycle management machinery.
              target.$on('$destroy', unsubscribe);

            } else if ('$on' in target) {

              throw new Error(
                'Passed an AngularJS scope to ngRedux that did not ' +
                  'belong to the same AngularJS injector that owns ngRedux. ' +
                  'You are probably in deep trouble.'
              );

            } else {

              // Assume `target` is an AngularJS viewmodel instance.
              // Unfortunately, there is no inheritance chain we can check
              // to be sure one way or the other. We'll define an
              // $onDestroy() hook that AngularJS will see, if it's
              // looking, and warn during application teardown if anything
              // leaks through.

              if (!target.constructor || target.constructor === Object) {
                $log.warn(
                  'Passed a plain object to ngRedux. ngRedux technically ' +
                    'supports passing any object as a target. Cannot automatically, ' +
                    'unlisten. Please remember to unsubscribe at the end of the ' +
                    "target object's lifetime."
                );
              }

              if (targets.has(target)) {

                // we've already seen this target, so we don't need to
                // redo our monkeypatch. It's sufficient to register the
                // additional listener and move on.
                targets.get(target).subscriptions.push(unsubscribe);

              } else {

                // cache the ownProperty descriptor of the '$onDestroy'
                // property we are about to monkeypatch (if it exists). If
                // the target is not actually an AngularJS viewmodel
                // instance, it will outlive the rootScope, and we will
                // use this cached descriptor to restore the target to its
                // original state.
                //
                // It would be pretty weird for an object which is not an
                // AngularJS viewmodel to have an '$onDestroy' ownProperty,
                // and a developer who passed such an object as a target to
                // `ngRedux.connect(...)(target)` would certainly be asking
                // for trouble, but the possibility is well within the
                // realm of imagination, and we must be as defensive as
                // possible when mutating objects of unknown provenance.

                destructorDescriptor = Object.getOwnPropertyDescriptor(
                  target,
                  '$onDestroy'
                );

                if (destructorDescriptor && !destructorDescriptor.configurable) {
                  throw new Error(
                    'Passed a target to ngRedux with a non-configurable ' +
                      '`$onDestroy` property. This is a pretty strange thing to ' +
                      'do. If you are procedurally setting $onDestroy on an ' +
                      'AngularJS viewmodel instance, you should use a normal ' +
                      'assignment (`=`) operator. If you need to use ' +
                      '`Object.defineProperty()` for metaprogramming, set ' +
                      '`configurable: true` on your property descriptor object.'
                  );
                }

                targets.set(target, {
                  subscriptions: [unsubscribe],
                  destructorDescriptor: destructorDescriptor
                });

                // retrieve from the prototype chain
                superDestructor = target.$onDestroy

                Object.defineProperty(target, '$onDestroy', {
                  // we may need to delete this later in `unsubscribeAll()`
                  configurable: true,
                  // compose the original destructor, if one
                  // exists, and unsubscribe.
                  get() {
                    return function $onDestroy () {

                      var targetInfo = targets.get(this)
                        , destructorDescriptor = targetInfo.destructorDescriptor
                        , destructor = destructorDescriptor
                          ? Object.prototype.hasOwnProperty.call(destructorDescriptor, 'value')
                            ? destructorDescriptor.value
                            : destructorDescriptor.get.call(this)
                          : superDestructor;

                      // Allow an ownProperty to override the method we
                      // retrieved from the prototype chain, if both exist.
                      
                      if (destructor) {
                        destructor.call(this);
                      }
                      unsubscribeAll(this);
                    }
                  },

                  // It is very important that we implement this as a
                  // getter/setter pair, so that we can handle the case
                  // where `$onDestroy()` is procedurally defined after
                  // a call to `ngRedux.connect()` in the body of an
                  // old-school constructor function style controller
                  // definition, e.g.:
                  //
                  // function FooController($ngRedux) {
                  //   $ngRedux.connect(...)(this);
                  //   // don't want to accidentally overwrite the secret
                  //   // $onDestory() handler that the monkeypatched
                  //   // ngRedux.connect() applied, want to compose it!
                  //   this.$onDestroy = function () { ... };
                  // }
                  //
                  // We handle this by writing the incoming $onDestroy()
                  // method into the destructorDescription, where it can
                  // be ready out in the getter and composed with our
                  // unsubscribe logic (see above).

                  set (value) {
                    var targetInfo = targets.get(target)
                      ,  destructorDescriptor = targetInfo.destructorDescriptor;
                    if (destructorDescriptor) {

                      // Why would $onDestroy() ever be a setter?
                      // Well, we made it one here, and we might need
                      // to compose with someone else who has the same
                      // bright idea. Pedantry is of the essence.
                      if (destructorDescriptor.set) {
                        destructorDescriptor.set.call(this, value);
                      } else {
                        destructorDescriptor.value = value;
                      }

                    } else {

                      // Build a novel destructorDescriptor,
                      // imitating normal `=` assignment
                      targetInfo.destructorDescriptor = {
                        value: value,
                        writable: true,
                        enumerable: true,
                        configurable: true,
                      };

                    }
                  },
                })
              }
            }

            return unsubscribe;
          }
        };

        // It's virtually certain that nobody would intentionally use
        // $ngRedux and intend for it to outlive the AngularJS injector in
        // which $ngRedux is itself hosted. Even if a developer using
        // $ngRedux inexplicably evades all earlier attempts to detect and
        // avert misuse, we can still clean up leaked subscriptions at the
        // end of the injector's lifecycle.
        $rootScope.$on('$destroy', function () {
          var allTargets = targets.keys()
            , i
            , target;

          if (targets.size > 0) {
            $log.error('ngRedux: automatic subscription cleanup failed.');
            $log.info(
              'Hint: did you manually instantiate an AngularJS controller, ' +
                'but forget to call $onDestroy() when you were done with ' +
                'it? Did you remember to close all modals and await ' +
                'their animations?'
            );

            for (i = 0; i < allTargets.length; i++) {
              target = allTargets[0];
              unsubscribeAll(target);
            }
          }
        });

        return $ngRedux;
      }
    ]
  );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants