Repository: sveltejs/svelte (JavaScript)
Date: 2017-08-31 17:26:49+00:00
Task type: Bug Fix

Issue:
Recent change: `set`, `get` no longer functional in `ondestroy`
Somewhere between `1.29` and `1.32` (I think mostly in #755), `set` and `get` turned into `noop` by the time `ondestroy` is called.

I have some use cases for calling both those methods (especially `get`) in `ondestroy`, so it would be nice to leave them functional until after it was called.


Issue discussion:
From what I gathered in gitter, your use case was mainly storing listener deregisterers on the component and then calling them in `ondestroy`. I recently had to deal with something like that, and how I handled it was

```
oncreate() {
  this.on('destroy', registerListenerAndReturnDeregisterer())
}
```

i.e., I didn't officially register an `ondestroy` hook, I just attached a `destroy` lifecycle event handler in the `oncreate` hook, where I already had access to the deregisterer. Not to say that this is the only reason for wanting to access `get`, or that what you're suggesting isn't a good idea - it's just another idea that might be useful.
That is an excellent tip.
I just ran into this issue as well and I'm not 100% on how to proceed.  I see that @TehShrike and @Conduitry  have a work around which is great.  However, is this going to be the expected behavior of `ondestroy` moving forward?  Or is it a bug?
I guess we just need to figure out exactly what the behaviour *should* be, and implement it. E.g. is it just making [this change](https://github.com/sveltejs/svelte/blob/12e9a9276cc337dc8291ec55d555bb86996bbcb7/src/shared/index.js#L7-L9)?

```diff
export function destroy(detach) {
-  this.destroy = this.set = this.get = noop;
+ this.destroy = noop;
  this.fire('destroy');
+  this.set = this.get = noop;
```
Not allowing any state changes when a component is being unmounted to me seems 
pretty consistent with most other view libraries. For instance, in React `setState` is not allowed in `componentWillUnmount`.

With regards to `this.get` though - I'm not entirely sure.  

I think I was only using `this.get` often in `ondestroy` was because it was available.  At least in the use cases I've come across `this.get` hasn't been strictly necessary though.

I'd opt for (but I'm sure others might have a much better idea here): 

```
export function destroy(detach) {
  this.destroy = this.set = noop;
  this.fire('destroy');
  this.get = noop;
  ...
```

My 2 cents, and thanks for everything Rich!  
I actually do have a use case for `set` in `ondestroy` - I have a component that tracks visibility, and uses two-way binding to keep a `visible` property up to date, and I make sure to set `visible` to false in `ondestroy` just so there isn't some state around somewhere that thinks that item is still visible somehow.

I would rather the component was still "fully functional" until after `ondestroy`.

Solution patch:
diff --git a/src/shared/index.js b/src/shared/index.js
--- a/src/shared/index.js
+++ b/src/shared/index.js
@@ -5,8 +5,9 @@ export * from './transitions.js';
 export * from './utils.js';
 
 export function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();


Test patch:
diff --git a/test/js/samples/collapses-text-around-comments/expected-bundle.js b/test/js/samples/collapses-text-around-comments/expected-bundle.js
--- a/test/js/samples/collapses-text-around-comments/expected-bundle.js
+++ b/test/js/samples/collapses-text-around-comments/expected-bundle.js
@@ -38,8 +38,9 @@ function setAttribute(node, attribute, value) {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/computed-collapsed-if/expected-bundle.js b/test/js/samples/computed-collapsed-if/expected-bundle.js
--- a/test/js/samples/computed-collapsed-if/expected-bundle.js
+++ b/test/js/samples/computed-collapsed-if/expected-bundle.js
@@ -14,8 +14,9 @@ function assign(target) {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/css-media-query/expected-bundle.js b/test/js/samples/css-media-query/expected-bundle.js
--- a/test/js/samples/css-media-query/expected-bundle.js
+++ b/test/js/samples/css-media-query/expected-bundle.js
@@ -34,8 +34,9 @@ function setAttribute(node, attribute, value) {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js
--- a/test/js/samples/each-block-changed-check/expected-bundle.js
+++ b/test/js/samples/each-block-changed-check/expected-bundle.js
@@ -47,8 +47,9 @@ function createText(data) {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/event-handlers-custom/expected-bundle.js b/test/js/samples/event-handlers-custom/expected-bundle.js
--- a/test/js/samples/event-handlers-custom/expected-bundle.js
+++ b/test/js/samples/event-handlers-custom/expected-bundle.js
@@ -34,8 +34,9 @@ function createText(data) {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/if-block-no-update/expected-bundle.js b/test/js/samples/if-block-no-update/expected-bundle.js
--- a/test/js/samples/if-block-no-update/expected-bundle.js
+++ b/test/js/samples/if-block-no-update/expected-bundle.js
@@ -38,8 +38,9 @@ function createComment() {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/if-block-simple/expected-bundle.js b/test/js/samples/if-block-simple/expected-bundle.js
--- a/test/js/samples/if-block-simple/expected-bundle.js
+++ b/test/js/samples/if-block-simple/expected-bundle.js
@@ -38,8 +38,9 @@ function createComment() {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/legacy-input-type/expected-bundle.js b/test/js/samples/legacy-input-type/expected-bundle.js
--- a/test/js/samples/legacy-input-type/expected-bundle.js
+++ b/test/js/samples/legacy-input-type/expected-bundle.js
@@ -32,8 +32,9 @@ function setInputType(input, type) {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/non-imported-component/expected-bundle.js b/test/js/samples/non-imported-component/expected-bundle.js
--- a/test/js/samples/non-imported-component/expected-bundle.js
+++ b/test/js/samples/non-imported-component/expected-bundle.js
@@ -28,8 +28,9 @@ function createText(data) {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
--- a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
+++ b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
@@ -14,8 +14,9 @@ function assign(target) {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/setup-method/expected-bundle.js b/test/js/samples/setup-method/expected-bundle.js
--- a/test/js/samples/setup-method/expected-bundle.js
+++ b/test/js/samples/setup-method/expected-bundle.js
@@ -14,8 +14,9 @@ function assign(target) {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/js/samples/use-elements-as-anchors/expected-bundle.js b/test/js/samples/use-elements-as-anchors/expected-bundle.js
--- a/test/js/samples/use-elements-as-anchors/expected-bundle.js
+++ b/test/js/samples/use-elements-as-anchors/expected-bundle.js
@@ -38,8 +38,9 @@ function createComment() {
 }
 
 function destroy(detach) {
-	this.destroy = this.set = this.get = noop;
+	this.destroy = noop;
 	this.fire('destroy');
+	this.set = this.get = noop;
 
 	if (detach !== false) this._fragment.unmount();
 	this._fragment.destroy();
diff --git a/test/runtime/samples/set-in-onrender/_config.js b/test/runtime/samples/set-in-oncreate/_config.js
similarity index 100%
rename from test/runtime/samples/set-in-onrender/_config.js
rename to test/runtime/samples/set-in-oncreate/_config.js
diff --git a/test/runtime/samples/set-in-onrender/main.html b/test/runtime/samples/set-in-oncreate/main.html
similarity index 100%
rename from test/runtime/samples/set-in-onrender/main.html
rename to test/runtime/samples/set-in-oncreate/main.html
diff --git a/test/runtime/samples/set-in-ondestroy/_config.js b/test/runtime/samples/set-in-ondestroy/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/set-in-ondestroy/_config.js
@@ -0,0 +1,26 @@
+export default {
+	'skip-ssr': true,
+
+	data: {
+		foo: 1
+	},
+
+	test(assert, component) {
+		const values = [];
+		let valueOnDestroy;
+
+		component.on('destroy', () => {
+			component.set({ foo: 2 });
+			valueOnDestroy = component.get('foo');
+		});
+
+		component.observe('foo', foo => {
+			values.push(foo);
+		});
+
+		component.destroy();
+
+		assert.deepEqual(values, [1, 2]);
+		assert.equal(valueOnDestroy, 2);
+	}
+};
diff --git a/test/runtime/samples/set-in-ondestroy/main.html b/test/runtime/samples/set-in-ondestroy/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/set-in-ondestroy/main.html
@@ -0,0 +1 @@
+empty