-
Notifications
You must be signed in to change notification settings - Fork 177
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
Comments
Automatic unsubscribe isn't possible but AngularJS does have a component/scope lifecycle that you can hook into via if you're using AngularJS components, you can add a method called So why can't this be done automatically? because a user doesn't have to connect to the actual component! You can bind to |
If you are targeting browsers that support 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 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;
}
]
); |
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:
to https://github.com/angular-redux/ng-redux/blob/master/src/components/connector.js#L58 ?
The text was updated successfully, but these errors were encountered: