Repository: sveltejs/svelte (JavaScript)
Date: 2017-08-06 02:20:45+00:00
Task type: Bug Fix

Issue:
"Uncaught TypeError: Cannot read property 'update' of null" when calling set in an event after destroy
This manifests when using an evented object that has a listener that destroys the Component & a subsequent listener that sets a value on the Component.

https://svelte.technology/repl?version=1.27.0&gist=16c53dce350c0a67c6bb1ff8db61ab88


Issue discussion:
@btakita what should the intended behaviour be? And what's up man lol.
Would a dev mode error like this solve the problem?

> You cannot call `.set(...)` on a component that has been destroyed


I'd rather it not throw an error.

Right now, I shimmed a method that checks to see if the component is "active" before calling `set`.

```
export function set__svelte(C, ...rest) {
  return C._fragment ? C.set(...rest) : C
}
export const set = set__svelte
```
What's the situation in which `set` is being called after `destroy`? Just wondering if there's a different way of getting at this, without adding a `this._destroyed` check for each `set` call (which certainly isn't the worst thing in the world, but good to avoid it if it's masking a separate problem)
It's difficult to avoid this issue using events that are ordered by definition, as previously defined events may triggers `destroy` before a later defined event triggers `set`.

Here are three areas where the logic could be handled:

* Event handler ordering - imposes difficult & otherwise unnecessary constraints on the programmer
* Guard clause in the event handler - Which is what I'm doing right now. It creates an otherwise unnecessary helper function, though it's relatively painless & could be a useful "seam" later on
* `console.warn`
* `this._destroyed` guard clause in svelte

I'm ok with `console.warn`, even though it would still encourage me to have a helper function. Letting it throw an error would require debugging for somebody unfamiliar with the problem. Maybe put a link to this ticket, or a section in the Readme to resolve?

I would slightly prefer the `this._destroyed` guard but now that there's a helper function, it's not that big of a deal.
I debounce some method calls to the next animation frame, and in those methods I've been forced to check for `this._destroyed` before continuing.

Solution patch:
diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts
--- a/src/generators/dom/index.ts
+++ b/src/generators/dom/index.ts
@@ -174,8 +174,8 @@ export default function dom(
 		? `@proto `
 		: deindent`
 		{
-			${['get', 'fire', 'observe', 'on', 'set']
-				.map(n => `${n}: @${n}`)
+			${['destroy', 'get', 'fire', 'observe', 'on', 'set', 'teardown']
+				.map(n => `${n}: @${n === 'teardown' ? 'destroy' : n}`)
 				.join(',\n')}
 		}`;
 
@@ -207,11 +207,11 @@ export default function dom(
 			};
 
 			this._handlers = Object.create( null );
+			${templateProperties.ondestroy && `this._handlers.destroy = [@template.ondestroy]`}
 
 			this._root = options._root || this;
 			this._yield = options._yield;
 
-			this._destroyed = false;
 			${generator.stylesheet.hasStyles &&
 				options.css !== false &&
 				`if ( !document.getElementById( '${generator.stylesheet.id}-style' ) ) @add_css();`}
@@ -263,19 +263,6 @@ export default function dom(
 			${_set}
 		};
 
-		${name}.prototype.teardown = ${name}.prototype.destroy = function destroy ( detach ) {
-			if ( this._destroyed ) return${options.dev && ` console.warn( 'Component was already destroyed' )`};
-			this.fire( 'destroy' );
-			${templateProperties.ondestroy && `@template.ondestroy.call( this );`}
-
-			if ( detach !== false ) this._fragment.unmount();
-			this._fragment.destroy();
-			this._fragment = null;
-
-			this._state = {};
-			this._destroyed = true;
-		};
-
 		${templateProperties.setup && `@template.setup( ${name} );`}
 	`);
 
diff --git a/src/shared/index.js b/src/shared/index.js
--- a/src/shared/index.js
+++ b/src/shared/index.js
@@ -1,8 +1,27 @@
 import { assign } from './utils.js';
+import { noop } from './utils.js';
 export * from './dom.js';
 export * from './transitions.js';
 export * from './utils.js';
 
+export function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
+export function destroyDev(detach) {
+	destroy.call(this, detach);
+	this.destroy = function() {
+		console.warn('Component was already destroyed');
+	};
+}
+
 export function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -119,17 +138,21 @@ export function callAll(fns) {
 }
 
 export var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 export var protoDev = {
+	destroy: destroyDev,
 	get: get,
 	fire: fire,
 	observe: observeDev,
 	on: onDev,
-	set: set
+	set: set,
+	teardown: destroyDev
 };


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
@@ -37,6 +37,17 @@ function setAttribute(node, attribute, value) {
 	node.setAttribute(attribute, value);
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -128,11 +139,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 var template = (function () {
@@ -201,7 +214,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
 	if ( !document.getElementById( 'svelte-3590263702-style' ) ) add_css();
 
 	this._fragment = create_main_fragment( this._state, this );
@@ -222,16 +234,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
diff --git a/test/js/samples/collapses-text-around-comments/expected.js b/test/js/samples/collapses-text-around-comments/expected.js
--- a/test/js/samples/collapses-text-around-comments/expected.js
+++ b/test/js/samples/collapses-text-around-comments/expected.js
@@ -66,7 +66,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
 	if ( !document.getElementById( 'svelte-3590263702-style' ) ) add_css();
 
 	this._fragment = create_main_fragment( this._state, this );
@@ -87,16 +86,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
\ No newline at end of file
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
@@ -13,6 +13,17 @@ function assign(target) {
 	return target;
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -104,11 +115,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 function recompute ( state, newState, oldState, isInitial ) {
@@ -155,8 +168,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -175,16 +186,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
diff --git a/test/js/samples/computed-collapsed-if/expected.js b/test/js/samples/computed-collapsed-if/expected.js
--- a/test/js/samples/computed-collapsed-if/expected.js
+++ b/test/js/samples/computed-collapsed-if/expected.js
@@ -44,8 +44,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -64,16 +62,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
\ No newline at end of file
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
@@ -33,6 +33,17 @@ function setAttribute(node, attribute, value) {
 	node.setAttribute(attribute, value);
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -124,11 +135,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 function encapsulateStyles ( node ) {
@@ -181,7 +194,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
 	if ( !document.getElementById( 'svelte-2363328337-style' ) ) add_css();
 
 	this._fragment = create_main_fragment( this._state, this );
@@ -201,16 +213,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
diff --git a/test/js/samples/css-media-query/expected.js b/test/js/samples/css-media-query/expected.js
--- a/test/js/samples/css-media-query/expected.js
+++ b/test/js/samples/css-media-query/expected.js
@@ -50,7 +50,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
 	if ( !document.getElementById( 'svelte-2363328337-style' ) ) add_css();
 
 	this._fragment = create_main_fragment( this._state, this );
@@ -70,16 +69,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
\ No newline at end of file
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
@@ -46,6 +46,17 @@ function createText(data) {
 	return document.createTextNode(data);
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -137,11 +148,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 function create_main_fragment ( state, component ) {
@@ -301,8 +314,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -321,16 +332,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js
--- a/test/js/samples/each-block-changed-check/expected.js
+++ b/test/js/samples/each-block-changed-check/expected.js
@@ -157,8 +157,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -177,16 +175,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
\ No newline at end of file
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
@@ -1,3 +1,5 @@
+function noop() {}
+
 function assign(target) {
 	var k,
 		source,
@@ -31,6 +33,17 @@ function createText(data) {
 	return document.createTextNode(data);
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -122,11 +135,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 var template = (function () {
@@ -190,8 +205,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -209,16 +222,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
diff --git a/test/js/samples/event-handlers-custom/expected.js b/test/js/samples/event-handlers-custom/expected.js
--- a/test/js/samples/event-handlers-custom/expected.js
+++ b/test/js/samples/event-handlers-custom/expected.js
@@ -61,8 +61,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -80,16 +78,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
\ No newline at end of file
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
@@ -37,6 +37,17 @@ function createComment() {
 	return document.createComment('');
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -128,11 +139,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 function create_main_fragment ( state, component ) {
@@ -236,8 +249,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -256,16 +267,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
diff --git a/test/js/samples/if-block-no-update/expected.js b/test/js/samples/if-block-no-update/expected.js
--- a/test/js/samples/if-block-no-update/expected.js
+++ b/test/js/samples/if-block-no-update/expected.js
@@ -101,8 +101,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -121,16 +119,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
\ No newline at end of file
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
@@ -37,6 +37,17 @@ function createComment() {
 	return document.createComment('');
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -128,11 +139,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 function create_main_fragment ( state, component ) {
@@ -212,8 +225,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -232,16 +243,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
diff --git a/test/js/samples/if-block-simple/expected.js b/test/js/samples/if-block-simple/expected.js
--- a/test/js/samples/if-block-simple/expected.js
+++ b/test/js/samples/if-block-simple/expected.js
@@ -77,8 +77,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -97,16 +95,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
\ No newline at end of file
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
@@ -1,5 +1,7 @@
 import Imported from 'Imported.html';
 
+function noop() {}
+
 function assign(target) {
 	var k,
 		source,
@@ -25,6 +27,17 @@ function createText(data) {
 	return document.createTextNode(data);
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -116,11 +129,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 var template = (function () {
@@ -182,8 +197,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	if ( !options._root ) {
 		this._oncreate = [];
 		this._beforecreate = [];
@@ -215,16 +228,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
diff --git a/test/js/samples/non-imported-component/expected.js b/test/js/samples/non-imported-component/expected.js
--- a/test/js/samples/non-imported-component/expected.js
+++ b/test/js/samples/non-imported-component/expected.js
@@ -61,8 +61,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	if ( !options._root ) {
 		this._oncreate = [];
 		this._beforecreate = [];
@@ -94,16 +92,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
\ No newline at end of file
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
@@ -13,6 +13,17 @@ function assign(target) {
 	return target;
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -104,11 +115,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 var template = (function () {
@@ -142,12 +155,11 @@ function SvelteComponent ( options ) {
 	};
 
 	this._handlers = Object.create( null );
+	this._handlers.destroy = [template.ondestroy];
 
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	var oncreate = template.oncreate.bind( this );
 
 	if ( !options._root ) {
@@ -177,17 +189,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-	template.ondestroy.call( this );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
diff --git a/test/js/samples/onrender-onteardown-rewritten/expected.js b/test/js/samples/onrender-onteardown-rewritten/expected.js
--- a/test/js/samples/onrender-onteardown-rewritten/expected.js
+++ b/test/js/samples/onrender-onteardown-rewritten/expected.js
@@ -31,12 +31,11 @@ function SvelteComponent ( options ) {
 	};
 
 	this._handlers = Object.create( null );
+	this._handlers.destroy = [template.ondestroy]
 
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	var oncreate = template.oncreate.bind( this );
 
 	if ( !options._root ) {
@@ -66,17 +65,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-	template.ondestroy.call( this );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
\ No newline at end of file
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
@@ -13,6 +13,17 @@ function assign(target) {
 	return target;
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -104,11 +115,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 var template = (function () {
@@ -157,8 +170,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -176,18 +187,6 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 template.setup( SvelteComponent );
 
 export default SvelteComponent;
diff --git a/test/js/samples/setup-method/expected.js b/test/js/samples/setup-method/expected.js
--- a/test/js/samples/setup-method/expected.js
+++ b/test/js/samples/setup-method/expected.js
@@ -46,8 +46,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -65,18 +63,6 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 template.setup( SvelteComponent );
 
-export default SvelteComponent;
+export default SvelteComponent;
\ No newline at end of file
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
@@ -37,6 +37,17 @@ function createComment() {
 	return document.createComment('');
 }
 
+function destroy(detach) {
+	this.destroy = this.set = noop;
+	this.fire('destroy');
+
+	if (detach !== false) this._fragment.unmount();
+	this._fragment.destroy();
+	this._fragment = null;
+
+	this._state = {};
+}
+
 function differs(a, b) {
 	return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
 }
@@ -128,11 +139,13 @@ function callAll(fns) {
 }
 
 var proto = {
+	destroy: destroy,
 	get: get,
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set
+	set: set,
+	teardown: destroy
 };
 
 function create_main_fragment ( state, component ) {
@@ -396,8 +409,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -416,16 +427,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
diff --git a/test/js/samples/use-elements-as-anchors/expected.js b/test/js/samples/use-elements-as-anchors/expected.js
--- a/test/js/samples/use-elements-as-anchors/expected.js
+++ b/test/js/samples/use-elements-as-anchors/expected.js
@@ -261,8 +261,6 @@ function SvelteComponent ( options ) {
 	this._root = options._root || this;
 	this._yield = options._yield;
 
-	this._destroyed = false;
-
 	this._fragment = create_main_fragment( this._state, this );
 
 	if ( options.target ) {
@@ -281,16 +279,4 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	dispatchObservers( this, this._observers.post, newState, oldState );
 };
 
-SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
-	if ( this._destroyed ) return;
-	this.fire( 'destroy' );
-
-	if ( detach !== false ) this._fragment.unmount();
-	this._fragment.destroy();
-	this._fragment = null;
-
-	this._state = {};
-	this._destroyed = true;
-};
-
 export default SvelteComponent;
\ No newline at end of file
diff --git a/test/runtime/samples/lifecycle-events/_config.js b/test/runtime/samples/lifecycle-events/_config.js
--- a/test/runtime/samples/lifecycle-events/_config.js
+++ b/test/runtime/samples/lifecycle-events/_config.js
@@ -1,7 +1,7 @@
 export default {
-	test ( assert, component ) {
-		assert.deepEqual( component.events, [ 'render' ]);
+	test(assert, component) {
+		assert.deepEqual(component.events, ['create']);
 		component.destroy();
-		assert.deepEqual( component.events, [ 'render', 'teardown' ]);
+		assert.deepEqual(component.events, ['create', 'destroy']);
 	}
 };
diff --git a/test/runtime/samples/lifecycle-events/main.html b/test/runtime/samples/lifecycle-events/main.html
--- a/test/runtime/samples/lifecycle-events/main.html
+++ b/test/runtime/samples/lifecycle-events/main.html
@@ -2,12 +2,12 @@
 
 <script>
 	export default {
-		oncreate () {
-			this.events = [ 'render' ];
+		oncreate() {
+			this.events = ['create'];
 		},
 
-		ondestroy () {
-			this.events.push( 'teardown' );
+		ondestroy() {
+			this.events.push('destroy');
 		}
 	};
-</script>
+</script>
\ No newline at end of file
diff --git a/test/runtime/samples/set-after-destroy/_config.js b/test/runtime/samples/set-after-destroy/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/set-after-destroy/_config.js
@@ -0,0 +1,10 @@
+export default {
+	data: {
+		x: 1
+	},
+
+	test(assert, component) {
+		component.destroy();
+		component.set({ x: 2 });
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/set-after-destroy/main.html b/test/runtime/samples/set-after-destroy/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/set-after-destroy/main.html
@@ -0,0 +1 @@
+<div>{{x}}</div>
\ No newline at end of file