Repository: sveltejs/svelte (JavaScript)
Date: 2018-05-01 01:36:35+00:00
Task type: Bug Fix

Issue:
Defining store computed property after it is referenced in a template causes initial value to be undefined
It does not seem like store can be used in the repl (https://github.com/sveltejs/svelte.technology/issues/213), so I'll explain the issue here.

I am mixing in computed properties into a store. It does not seem like the initial value of newly created store computed properties bubble up to the template.

```html
<p>{{$name}}</p>

<script>
  export default {
    oncreate() {
      this.store.compute('name', ['user'], user => user && user.name)
      this.store.set({user: {name: 'Joe'}})
    }
  }
</script>
```

To alleviate this issue, I added a `compute` wrapper:

```javascript
export function compute(store, name, deps, fn) {
  const values__deps = []
  for (let i=0; i < deps.length; i++) {
    values__deps.push(store.get(deps[i]))
  }
  const __set = {}
  __set[name] = fn(...values__deps)
  store.set(__set)
  store.compute(name, deps, fn)
  return store
}
```


Issue discussion:
With the v2 upgrade, the `compute` helper function is now:

```javascript
export function compute(store, name, deps, fn) {
  const values__deps = []
  const state = store.get()
  for (let i=0; i < deps.length; i++) {
    values__deps.push(state[deps[i]])
  }
  const __set = {}
  __set[name] = fn(...values__deps)
  store.set(__set)
  store.compute(name, deps, fn)
  return store
}
```
Thanks — it looks like [this works in the current version](https://svelte.technology/repl?version=2.2.0&gist=d37e6159eefbcad5bbceb94be2908308) — it's possible that it was fixed since you opened this.

Let me know if I'm closing this issue in error.
It's a matter of the ordering. The issue occurs if you call `this.store.set` before `this.store.compute`...

https://svelte.technology/repl?version=2.2.0&gist=a0548b9dafee301e890541a0d1ee4c6a

Solution patch:
diff --git a/store.js b/store.js
--- a/store.js
+++ b/store.js
@@ -20,24 +20,24 @@ function Store(state, options) {
 }
 
 assign(Store.prototype, {
-	_add: function(component, props) {
+	_add(component, props) {
 		this._dependents.push({
 			component: component,
 			props: props
 		});
 	},
 
-	_init: function(props) {
-		var state = {};
-		for (var i = 0; i < props.length; i += 1) {
-			var prop = props[i];
+	_init(props) {
+		const state = {};
+		for (let i = 0; i < props.length; i += 1) {
+			const prop = props[i];
 			state['$' + prop] = this._state[prop];
 		}
 		return state;
 	},
 
-	_remove: function(component) {
-		var i = this._dependents.length;
+	_remove(component) {
+		let i = this._dependents.length;
 		while (i--) {
 			if (this._dependents[i].component === component) {
 				this._dependents.splice(i, 1);
@@ -46,14 +46,52 @@ assign(Store.prototype, {
 		}
 	},
 
-	_sortComputedProperties: function() {
-		var computed = this._computed;
-		var sorted = this._sortedComputedProperties = [];
-		var visited = blankObject();
-		var currentKey;
+	_set(newState, changed) {
+		const previous = this._state;
+		this._state = assign(assign({}, previous), newState);
+
+		for (let i = 0; i < this._sortedComputedProperties.length; i += 1) {
+			this._sortedComputedProperties[i].update(this._state, changed);
+		}
+
+		this.fire('state', {
+			changed,
+			previous,
+			current: this._state
+		});
+
+		const dependents = this._dependents.slice(); // guard against mutations
+		for (let i = 0; i < dependents.length; i += 1) {
+			const dependent = dependents[i];
+			const componentState = {};
+			dirty = false;
+
+			for (let j = 0; j < dependent.props.length; j += 1) {
+				const prop = dependent.props[j];
+				if (prop in changed) {
+					componentState['$' + prop] = this._state[prop];
+					dirty = true;
+				}
+			}
+
+			if (dirty) dependent.component.set(componentState);
+		}
+
+		this.fire('update', {
+			changed,
+			previous,
+			current: this._state
+		});
+	},
+
+	_sortComputedProperties() {
+		const computed = this._computed;
+		const sorted = this._sortedComputedProperties = [];
+		const visited = blankObject();
+		let currentKey;
 
 		function visit(key) {
-			var c = computed[key];
+			const c = computed[key];
 
 			if (c) {
 				c.deps.forEach(dep => {
@@ -71,26 +109,25 @@ assign(Store.prototype, {
 			}
 		}
 
-		for (var key in this._computed) {
+		for (const key in this._computed) {
 			visit(currentKey = key);
 		}
 	},
 
-	compute: function(key, deps, fn) {
-		var store = this;
-		var value;
+	compute(key, deps, fn) {
+		let value;
 
-		var c = {
-			deps: deps,
-			update: function(state, changed, dirty) {
-				var values = deps.map(function(dep) {
+		const c = {
+			deps,
+			update: (state, changed, dirty) => {
+				const values = deps.map(dep => {
 					if (dep in changed) dirty = true;
 					return state[dep];
 				});
 
 				if (dirty) {
-					var newValue = fn.apply(null, values);
-					if (store._differs(newValue, value)) {
+					const newValue = fn.apply(null, values);
+					if (this._differs(newValue, value)) {
 						value = newValue;
 						changed[key] = true;
 						state[key] = value;
@@ -99,63 +136,33 @@ assign(Store.prototype, {
 			}
 		};
 
-		c.update(this._state, {}, true);
-
 		this._computed[key] = c;
 		this._sortComputedProperties();
+
+		const state = assign({}, this._state);
+		const changed = {};
+		c.update(state, changed, true);
+		this._set(state, changed);
 	},
 
-	fire: fire,
+	fire,
 
-	get: get,
+	get,
 
-	on: on,
+	on,
 
-	set: function(newState) {
-		var oldState = this._state,
-			changed = this._changed = {},
-			dirty = false;
+	set(newState) {
+		const oldState = this._state;
+		const changed = this._changed = {};
+		let dirty = false;
 
-		for (var key in newState) {
-			if (this._computed[key]) throw new Error("'" + key + "' is a read-only property");
+		for (const key in newState) {
+			if (this._computed[key]) throw new Error(`'${key}' is a read-only property`);
 			if (this._differs(newState[key], oldState[key])) changed[key] = dirty = true;
 		}
 		if (!dirty) return;
 
-		this._state = assign(assign({}, oldState), newState);
-
-		for (var i = 0; i < this._sortedComputedProperties.length; i += 1) {
-			this._sortedComputedProperties[i].update(this._state, changed);
-		}
-
-		this.fire('state', {
-			changed: changed,
-			current: this._state,
-			previous: oldState
-		});
-
-		var dependents = this._dependents.slice(); // guard against mutations
-		for (var i = 0; i < dependents.length; i += 1) {
-			var dependent = dependents[i];
-			var componentState = {};
-			dirty = false;
-
-			for (var j = 0; j < dependent.props.length; j += 1) {
-				var prop = dependent.props[j];
-				if (prop in changed) {
-					componentState['$' + prop] = this._state[prop];
-					dirty = true;
-				}
-			}
-
-			if (dirty) dependent.component.set(componentState);
-		}
-
-		this.fire('update', {
-			changed: changed,
-			current: this._state,
-			previous: oldState
-		});
+		this._set(newState, changed);
 	}
 });
 


Test patch:
diff --git a/test/runtime/samples/store-computed-oncreate/_config.js b/test/runtime/samples/store-computed-oncreate/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-computed-oncreate/_config.js
@@ -0,0 +1,11 @@
+import { Store } from '../../../../store.js';
+
+export default {
+	'skip-ssr': true,
+
+	store: new Store(),
+
+	html: `
+		<h1>Hello Brian!</h1>
+	`
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-computed-oncreate/main.html b/test/runtime/samples/store-computed-oncreate/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-computed-oncreate/main.html
@@ -0,0 +1,10 @@
+<h1>Hello {$name}!</h1>
+
+<script>
+	export default {
+		oncreate() {
+			this.store.set({ user: { name: 'Brian' } });
+			this.store.compute('name', ['user'], user => user && user.name);
+		}
+	};
+</script>
\ No newline at end of file