{"action":"created","issue":{"url":"https://api.github.com/repos/christianalfoni/cerebral/issues/115","labels_url":"https://api.github.com/repos/christianalfoni/cerebral/issues/115/labels{/name}","comments_url":"https://api.github.com/repos/christianalfoni/cerebral/issues/115/comments","events_url":"https://api.github.com/repos/christianalfoni/cerebral/issues/115/events","html_url":"https://github.com/christianalfoni/cerebral/issues/115","id":108703262,"number":115,"title":"Suggestion on computed functions","user":{"login":"christianalfoni","id":3956929,"avatar_url":"https://avatars.githubusercontent.com/u/3956929?v=3","gravatar_id":"","url":"https://api.github.com/users/christianalfoni","html_url":"https://github.com/christianalfoni","followers_url":"https://api.github.com/users/christianalfoni/followers","following_url":"https://api.github.com/users/christianalfoni/following{/other_user}","gists_url":"https://api.github.com/users/christianalfoni/gists{/gist_id}","starred_url":"https://api.github.com/users/christianalfoni/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/christianalfoni/subscriptions","organizations_url":"https://api.github.com/users/christianalfoni/orgs","repos_url":"https://api.github.com/users/christianalfoni/repos","events_url":"https://api.github.com/users/christianalfoni/events{/privacy}","received_events_url":"https://api.github.com/users/christianalfoni/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":8,"created_at":"2015-09-28T17:13:05Z","updated_at":"2015-09-29T09:42:23Z","closed_at":null,"body":"I have been working on an idea where you can compute state more easily. And I think I might have something here. If this makes sense it will probably be implemented into Cerebral and can work with any immutable state store.\r\n\r\n```js\r\nconst model = Model({\r\n  messages: {},\r\n  displayedMessages: function (ids = [], get) {\r\n    return ids.map((id) => get(['messages', id]));\r\n  }\r\n});\r\n```\r\n\r\n### Benefits\r\n\r\n1. You can define it with a normal function\r\n2. You do not have to define its dependencies, it will track it based on values grabbed using the \"get\" function\r\n3. It will only rerun when its value or any of its previously grabbed state has changed\r\n4. The great benefit here is that it will track the state you actually extract. An example of this is 500 messages and you display 10 of them. It will only rerun if any of these 10 actually change, not whenever any of the other messages changes, which is typical behaviour. It will of course also rerun if you change the value on the computed function\r\n\r\n### How to use\r\n\r\nSet data using action:\r\n```js\r\nfunction myAction (input, state) {\r\n  state.set('displayedMessages', ['123']);\r\n  state.push('displayedMessages', '456');\r\n}\r\n```\r\n\r\nUse computed value in component:\r\n```js\r\nexport default Component({\r\n  messages: ['displayedMessages']\r\n}, (props) => (\r\n  <div>\r\n    {props.messages.length}\r\n  </div>\r\n));\r\n```\r\n### How it works\r\n\r\n1. When Cerebral initializes the tree it will extract the functions from the tree and replace them with \"null\" values\r\n2. Cerebral keeps its own registry of the paths to these computed values\r\n3. When an action or component tries to `get` values from this path the function will run\r\n4. The value stored in the state tree will be grabbed and passed to the function. If it is `null` it will use the default value defined in the computed function\r\n5. When the computed `get` function is used it will cache the returned values and their paths\r\n6. The returned value from the computed function is also cached\r\n7. When Cerebrals `get` method is used the computed function will do a shallow check on the passed value and all cached path/values. If nothing has changed it will return the cached computed value, if not it will run it again\r\n8. Any state mutations done to the computed state will be put into the tree itself\r\n\r\nSo basically instead of listening to top level changes on cursors, we do shallow checks on the state extracted when the computed function was last run.\r\n\r\n### A practical example\r\n\r\n```js\r\nconst model = Model({\r\n  messages: {},\r\n  displayedMessages: function (ids = [], get) {\r\n    return ids.map((id) => get(['messages', id]));    \r\n  }\r\n});\r\n```\r\n\r\nCerebral will extract the function and keep a reference to its path. The state store will look like this:\r\n\r\n```js\r\n{\r\n  messages: {},\r\n  displayedMessages: null\r\n}\r\n```\r\n\r\nA component grabs the state, which will run the function. Since we pass in null value from the state store and there are no path/values defined with the computed `get` function, it will return the default value, which in this case is an empty array :\r\n\r\n```js\r\nexport default Component({\r\n  messages: ['displayedMessages']\r\n}, (props) => (\r\n  <div>\r\n    {props.messages.length}\r\n  </div>\r\n));\r\n```\r\nSince the result of the function is an empty array, this DIV will display `0`.\r\n\r\nA signal runs this action:\r\n\r\n```js\r\nfunction addMessage (input, state) {\r\n  state.set(['messages', input.message.id], input.message);\r\n  state.set(['messagesDisplayed'], [input.message.id]); // [\"123\"]\r\n}\r\n```\r\n\r\nThe signal tells the UI to update and the same component will run Cerebrals `get` method to grab *messagesDisplayed*. Since the value inside the state store is now changed to `['123']` the computed function will run again:\r\n\r\n```js\r\nexport default Component({\r\n  messages: ['displayedMessages']\r\n}, (props) => (\r\n  <div>\r\n    {props.messages.length}\r\n  </div>\r\n));\r\n```\r\nNow it displays **1** and you would have access to the data of the message.\r\n\r\nNow you change the message using:\r\n\r\n```js\r\nfunction updateMessage (input, state) {\r\n  state.merge(['messages', input.id], input.update);\r\n}\r\n```\r\n\r\nThe signal again tells UI to update and the component will again try to use Cerebral `get` method to update its state. Now the array `['123']` inside the state store is the same, but the path/value used with the computed functions `get` function previously is now not the same anymore. So it will run again and give the new result.\r\n\r\n### Get, export and import\r\nWhen using Cerebrals `get` method it will verify computed paths part of the \"get\" and replace these values with the cached value of the computed function. When using *export* you will not get the computed values, but the actual values from the state store. Example of this would be `['123']`. When you use *import* Cerebral already handles this as a deep merge. This would just set the new value of a computed function and rerun it.\r\n\r\n### Caveat\r\nThere is one thing that is not ideal and that is the initial value of `null`. Using `set` it does not matter, but if you want to use `push`, `concat`, `merge` etc. that would cause an issue. There are two suggestions here:\r\n\r\n1. Cerebral inserts the missing value if the current value is `null`. This is based on the type of mutation. `push`, `unshift` etc. is array, and `merge` is object\r\n\r\n2. Cerebral exposes an `apply` mutation, which Baobab already has. Which works like this:\r\n\r\n```js\r\nfunction myAction (input, state) {\r\n  state.apply('somePath', function (value) {\r\n   return (value || []).concat(input.value);\r\n });\r\n}\r\n```\r\n\r\nThere is also an other thing. Lets say you do `controller.get()`, grab everything. Cerebral wants to insert all computed values. The problem with that is that the result from the state store is immutable. That means Cerebral has to rebuild the tree. That said, normally you point to specific paths and when you do not, it is rarely a bunch of computed values as child nodes. But this makes me think that this would benefit from being inside Baobab actually. Which also is a solution, any thoughts @Yomguithereal?\r\n\r\n### Recommendations on caveats\r\n\r\n1. Force `set` on computed nodes to change their values\r\n2. Force pointing directly to computed node to see its computed result\r\n3. Give debugger a dropdown to list and log out computed nodes \r\n\r\n### Summary\r\nOkay, so using shallow checking on immutable values and this smart computed  `get` function we get super performance, a very simple syntax and it works with any state store... also immutable-js. \r\n\r\nJust want to give a big thanks to @Yomguithereal for doing an excellent work with Baobab. Everything I have ever done related to state stores are inspired by his work.\r\n\r\nPlease give me your feedback on this. If you see scenarios it would not work. Or if you just do not like it :-)"},"comment":{"url":"https://api.github.com/repos/christianalfoni/cerebral/issues/comments/144007571","html_url":"https://github.com/christianalfoni/cerebral/issues/115#issuecomment-144007571","issue_url":"https://api.github.com/repos/christianalfoni/cerebral/issues/115","id":144007571,"user":{"login":"christianalfoni","id":3956929,"avatar_url":"https://avatars.githubusercontent.com/u/3956929?v=3","gravatar_id":"","url":"https://api.github.com/users/christianalfoni","html_url":"https://github.com/christianalfoni","followers_url":"https://api.github.com/users/christianalfoni/followers","following_url":"https://api.github.com/users/christianalfoni/following{/other_user}","gists_url":"https://api.github.com/users/christianalfoni/gists{/gist_id}","starred_url":"https://api.github.com/users/christianalfoni/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/christianalfoni/subscriptions","organizations_url":"https://api.github.com/users/christianalfoni/orgs","repos_url":"https://api.github.com/users/christianalfoni/repos","events_url":"https://api.github.com/users/christianalfoni/events{/privacy}","received_events_url":"https://api.github.com/users/christianalfoni/received_events","type":"User","site_admin":false},"created_at":"2015-09-29T09:42:23Z","updated_at":"2015-09-29T09:42:23Z","body":"@Yomguithereal I missed on important point in the description there. Whenever the computed functions runs its deps is reset. So you build up deps for each run of the function. On every `get` call to the computed value it will check if its current value or any deps on last run has changed. If not changes, return cached value or clear deps and run again to build up a new cached path/deps... if that makes sense :-)"}}