{"action":"created","issue":{"url":"https://api.github.com/repos/jorgebucaran/hyperapp/issues/749","repository_url":"https://api.github.com/repos/jorgebucaran/hyperapp","labels_url":"https://api.github.com/repos/jorgebucaran/hyperapp/issues/749/labels{/name}","comments_url":"https://api.github.com/repos/jorgebucaran/hyperapp/issues/749/comments","events_url":"https://api.github.com/repos/jorgebucaran/hyperapp/issues/749/events","html_url":"https://github.com/jorgebucaran/hyperapp/issues/749","id":354226896,"node_id":"MDU6SXNzdWUzNTQyMjY4OTY=","number":749,"title":"V2 Action API","user":{"login":"jorgebucaran","id":56996,"node_id":"MDQ6VXNlcjU2OTk2","avatar_url":"https://avatars1.githubusercontent.com/u/56996?v=4","gravatar_id":"","url":"https://api.github.com/users/jorgebucaran","html_url":"https://github.com/jorgebucaran","followers_url":"https://api.github.com/users/jorgebucaran/followers","following_url":"https://api.github.com/users/jorgebucaran/following{/other_user}","gists_url":"https://api.github.com/users/jorgebucaran/gists{/gist_id}","starred_url":"https://api.github.com/users/jorgebucaran/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jorgebucaran/subscriptions","organizations_url":"https://api.github.com/users/jorgebucaran/orgs","repos_url":"https://api.github.com/users/jorgebucaran/repos","events_url":"https://api.github.com/users/jorgebucaran/events{/privacy}","received_events_url":"https://api.github.com/users/jorgebucaran/received_events","type":"User","site_admin":false},"labels":[{"id":522275188,"node_id":"MDU6TGFiZWw1MjIyNzUxODg=","url":"https://api.github.com/repos/jorgebucaran/hyperapp/labels/Discussion","name":"Discussion","color":"7057ff","default":false},{"id":522275186,"node_id":"MDU6TGFiZWw1MjIyNzUxODY=","url":"https://api.github.com/repos/jorgebucaran/hyperapp/labels/Docs","name":"Docs","color":"e9e9ff","default":false}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":{"url":"https://api.github.com/repos/jorgebucaran/hyperapp/milestones/7","html_url":"https://github.com/jorgebucaran/hyperapp/milestone/7","labels_url":"https://api.github.com/repos/jorgebucaran/hyperapp/milestones/7/labels","id":3370575,"node_id":"MDk6TWlsZXN0b25lMzM3MDU3NQ==","number":7,"title":"V2","description":"","creator":{"login":"jorgebucaran","id":56996,"node_id":"MDQ6VXNlcjU2OTk2","avatar_url":"https://avatars1.githubusercontent.com/u/56996?v=4","gravatar_id":"","url":"https://api.github.com/users/jorgebucaran","html_url":"https://github.com/jorgebucaran","followers_url":"https://api.github.com/users/jorgebucaran/followers","following_url":"https://api.github.com/users/jorgebucaran/following{/other_user}","gists_url":"https://api.github.com/users/jorgebucaran/gists{/gist_id}","starred_url":"https://api.github.com/users/jorgebucaran/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jorgebucaran/subscriptions","organizations_url":"https://api.github.com/users/jorgebucaran/orgs","repos_url":"https://api.github.com/users/jorgebucaran/repos","events_url":"https://api.github.com/users/jorgebucaran/events{/privacy}","received_events_url":"https://api.github.com/users/jorgebucaran/received_events","type":"User","site_admin":false},"open_issues":5,"closed_issues":1,"state":"open","created_at":"2018-05-24T01:53:15Z","updated_at":"2018-10-03T03:27:27Z","due_on":"2018-11-01T07:00:00Z","closed_at":null},"comments":28,"created_at":"2018-08-27T08:27:01Z","updated_at":"2018-10-24T16:35:31Z","closed_at":null,"author_association":"OWNER","body":"## Summary\r\n\r\nV2 actions are different from V1 actions in several ways. Let's use this issue to discuss the new Action API in V2. My goal is to introduce the new API to everyone as it is currently being implemented in #726 and to gather your feedback and comments.\r\n\r\nI'm still open to small changes and corrections to this API, but I don't expect it to change significantly. If you would like to propose a different API, please create a new issue with a crosslink instead.\r\n\r\n## Background\r\n\r\nLet's begin by addressing the fact that V2 actions are used [declaratively](https://en.wikipedia.org/wiki/Declarative_programming) as opposed to V1 actions which are used [imperatively](https://en.wikipedia.org/wiki/Imperative_programming). I'm not going to discuss the merits of a declarative vs imperative paradigm in this post or why one could be considered better than the other (or not), but [start reading here](https://softwareengineering.stackexchange.com/questions/158715/are-small-amounts-of-functional-programming-understandable-by-non-fp-people/158721#158721) if you want to know what other people say about it.\r\n\r\nIn V1 we invoke actions in the same way we invoke any other function in JavaScript.\r\n\r\n```js\r\nmyAction(someData)\r\n```\r\n\r\nWe control _when_ and _where_ an action is invoked. We call actions inside event listeners within the view, inside other actions or outside the view from global event listeners, etc. We refer to the last approach as [interoperability](https://github.com/hyperapp/hyperapp#interoperability).\r\n\r\nIn V2 it's not us, but Hyperapp that invokes the actions. We're still in command, but we no longer control _when_ the action is invoked. We can only tell Hyperapp that the action should be invoked in response to (1) event listeners, (2) side effects or (3) subscriptions.\r\n\r\nAnother important difference with V1 is that actions are not \"wired\" to the app. This concept has been abandoned and as a result, the built-in [slice](https://github.com/hyperapp/hyperapp/tree/6669b7f893af6789596cf61fbfa02eea8656c441#nested-actions) mechanism has been removed. I'll describe how you can still use a flavor of slices (henceforth referred to as modules) to create applications [later in the issue](#modules).\r\n\r\n## Declarative actions\r\n\r\n### Signature\r\n\r\nWe can generalize the signature of every action like this.\r\n\r\n```js\r\n(state[, props], data) => newState\r\n```\r\n\r\n- _state_ is always the first argument to the action.\r\n- _props_ is an argument to the action _if_ the action is parameterized as later described in [parameterizing actions](#parameterizing-actions). If an action does not take any props, then _no_ props will be passed to the function!\r\n- _data_ is the last argument to the action. For events, it will be a DOM [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event) object. For side effects and subscriptions it will be the result of the effect or subscription.\r\n\r\nI'll introduce V2 _effects_ in relation to actions [later in this post](#creating-side-effects), but actions may also return a two-element array (henceforth referred to as tuple) whose first element is the new state and second is an object that represents the side effect.\r\n\r\n```js\r\n(state[, props], data) => [newState, nextEffect]\r\n```\r\n\r\n### Actions without arguments\r\n\r\nYou can write an action that takes no arguments and increments the state by one like this.\r\n\r\n```js\r\nconst Increment = state => state + 1\r\n```\r\n\r\nThe action name need not be capitalized, but _I_ prefer writing it that way. You can use the action in response to an event, e.g., a button click, like this.\r\n\r\n```jsx\r\n<button onClick={Increment}>Add One</button>\r\n```\r\n\r\nEither <samp>onclick</samp> and <samp>onClick</samp> are expected to be valid attributes in V2, but do check the release notes when V2 is officially released. If this looks similar to V1 is pure coincidence. There is no access to event listeners in V2 and <samp>onClick</samp> helps illustrate this difference by reminding you that you are not dealing with a traditional DOM event listener. For that reason I prefer <samp>onClick</samp>.\r\n\r\n<a name=\"parameterizing-actions\"></a>\r\n\r\n## Parameterizing actions\r\n\r\nHow do we pass data to an action? For example when iterating over an array to create a list of buttons or. Let's reuse the previous example. Here is how you can write an action that increments the state by some number.\r\n\r\n```js\r\nconst IncrementBy = (state, { number }) => state + number\r\n```\r\n\r\nAnd here is how you can use it.\r\n\r\n```jsx\r\n<button onClick={[IncrementBy, { number: 5 }]}>Add 5</button>\r\n```\r\n\r\nBeware of shortcuts! You may be tempted to curry IncrementBy like this.\r\n\r\n```jsx\r\n<button onClick={IncrementBy(5)}>Add 5</button>\r\n```\r\n\r\n<a name=\"parameterizing-actions-gotchas\"></a>\r\n\r\nWhy not? IncrementBy(5) returns an anonymous function that has no name, i.e., function.name is undefined. This means that a future Hyperapp extension adding debug features like Elm's time traveler or a logger would suffer or be unable to function properly. Unnamed anonymous functions are also more difficult to debug than regular actions.\r\n\r\nAnother disadvantage is that the algorithm responsible for diffing props will not be able to detect that you are using the same action when its arguments change. This means we'll end up touching the DOM when diffing event handlers when we could have avoided doing so.\r\n\r\n## Accessing the event data\r\n\r\nAs previously described, the last argument passed to an action is the data; the [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event) object for events, or the result of an effect or subscription.\r\n\r\n```js\r\nconst Increment = (state, event) => state + event.clientX\r\n```\r\n\r\nIf you need the `Event` object in a parameterized action, you can get it like this.\r\n\r\n```js\r\nconst IncrementBy = (state, { number }, event) => state + number\r\n```\r\n\r\n## Updating the state\r\n\r\nIn V2 the return value of an action is used to _replace_ the state. Unlike V1, the return value of an action is not [shallow-merged](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) with the current state but becomes the new global state. If you want the new state to be the product of a shallow merge with the current state, you must merge it yourself.\r\n\r\n```js\r\nconst Increment = state => ({ ...state, value: state.value + 1 })\r\n```\r\n\r\nor\r\n\r\n```js\r\nconst Increment = state => Object.assign({}, state, { value: state.value + 1 })\r\n```\r\n\r\nIf the object spread syntax used to merge objects is new to you [read this](https://davidwalsh.name/merge-objects).\r\n\r\nAnother option is to create actions using a function that wraps your original action and auto-merges the received state with its return value. I don't find this approach too appealing given how concise the `...` spread syntax is, but YMMV.\r\n\r\n```js\r\nconst auto = reducer => state => ({ ...state, ...reducer(state) })\r\n```\r\n\r\n```js\r\nconst Increment = auto(state => ({ value: state.value + 1 }))\r\n```\r\n\r\n<a name=\"creating-side-effects\"></a>\r\n\r\n## Creating side effects\r\n\r\nThis section explains how _effects_ and actions play together, not how to implement your own effects. There's a [dedicated issue](https://github.com/hyperapp/hyperapp/issues/750) for that if you are interested. For those familiar with Elm, we call \"effects\" what they call \"commands\".\r\n\r\nRemember you can't invoke actions in V2, but rather tell Hyperapp to do it for you using a declarative API? Producing side effects in V2 works the same way. No callbacks, promises, observables, channels, async or await. Those are powerful ideas, but we don't need them to manage side effects.\r\n\r\nAs a regular user, you won't be implementing your own effects either. It's possible, but it will be far more common to import existing effect adapters for what you need and use them as building blocks to create more advanced effects.\r\n\r\nEffects are usually, but not limited to, asynchronous operations. Here are some examples: creating HTTP requests, creating time delays, generating random numbers, [pushing or replacing](https://developer.mozilla.org/en-US/docs/Web/API/History_API) a URL to/from [Window.history](https://developer.mozilla.org/en-US/docs/Web/API/History), writing and reading to/from local/session [storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage), opening/closing a database connection as well as writing and reading to it using [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), requesting/exiting [fullscreen](https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API), [enqueing data](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send) over a WebSocket, etc.\r\n\r\nTechnically speaking, effects are plain objects. Much the same as a virtual node _represents_ a DOM element (but it isn't actually an element), an effect is an object that represents a side effect. Hyperapp runs the effect and calls a designated action with the result when the effect is done. To produce a side effect, an action must return a tuple with the state and the effect object.\r\n\r\n```js\r\nconst MakeSomeNoise = state => [newState, nextEffect]\r\n```\r\n\r\nHyperapp will use the first element of the array to update your application state and the second element to start the effect. Effect objects are usually created through a function, e.g., `Http.fetch`, `Random.generate`. From here on, whenever I refer to an effect I mean a function that returns an effect object.\r\n\r\nHere is an example that generates a random number.\r\n\r\n```js\r\nimport * as Random from \"@hyperapp/random\"\r\n\r\nconst NewFace = (state, newFace) => ({\r\n  ...state,\r\n  dieFace: Math.floor(newFace)\r\n})\r\n\r\nconst Roll = state => [\r\n  state,\r\n  Random.generate({ min: 1, max: 10, action: NewFace })\r\n]\r\n\r\napp({\r\n  init: Roll({ dieFace: 1 }),\r\n  view: state => (\r\n    <div>\r\n      <h1>{state.dieFace}</h1>\r\n      <button onClick={Roll}>Roll</button>\r\n    </div>\r\n  ),\r\n  container: document.body\r\n})\r\n```\r\n\r\nHere is an action that creates an HTTP request. Maybe it uses the fetch API under the hood and if not available, defaults to xmlhttprequest. Either way, this is transparent to us.\r\n\r\n```jsx\r\nimport * as Http from \"@hyperapp/http\"\r\n\r\nconst SendHttp = state => [\r\n  { ...state, error: null, fetching: true },\r\n  Http.fetch({\r\n    url: state.url,\r\n    action: SuccessResponse,\r\n    error: ErrorResponse\r\n  })\r\n]\r\n```\r\n\r\nEffects look a lot like Hyperapp components and because the underlying APIs are compatible, it's possible to use JSX to describe them. Here is the action that generates a random number again, but this time using JSX for the effect.\r\n\r\n```js\r\nimport * as Random from \"@hyperapp/random\"\r\n\r\nconst Roll = state => [\r\n  state,\r\n  <Random.generate min={1} max={10} action={NewFace} />\r\n]\r\n```\r\n\r\nHere is the action that creates an HTTP request using JSX for the effect.\r\n\r\n```jsx\r\nimport * as Http from \"@hyperapp/http\"\r\n\r\nconst SendHttp = state => [\r\n  { ...state, error: null, fetching: true },\r\n  <Http.fetch url={state.url} action={SuccessAction} error={ErrorAction} />\r\n]\r\n```\r\n\r\nAll you need to remember is that actions, in addition to updating your application state, may also return a tuple consisting of the state _and_ the effect object we want Hyperapp to run for us. When the effect comes back with a result, Hyperapp will run the specified action/s, e.g., `SuccessAction` if the request ended successfully or `ErrorAction` if there was an error in the example above.\r\n\r\nThe same rules that apply to every other action apply to these actions. As a result, an action can produce an effect that notifies an action that produces _another_ effect and so on, thus creating an effect sequence.\r\n\r\n<a name=\"modules\"></a>\r\n\r\n## Modules\r\n\r\nWe want to write independent code modules that export their own state, actions, and components and use them as a drop-in features in any application.\r\n\r\nV2 has no built-in mechanism to create object state namespaces, or _slices_, as they were referred to in V1, but that doesn't mean the concept is not available in V2 as I'll explain in this section.\r\n\r\n### Bottom-up approach\r\n\r\nThis section describes a bottom-up approach to building modules. You start with an standalone module exporting its own state, actions and even components and use it to create more complex modules until you reach the _top-level_ of the application. Then you should able to run your app with the initial state and view.\r\n\r\nHere is how to manually wire a standalone module to your application. \r\n\r\n```jsx\r\nconst counter = {\r\n  state: { value: 0 },\r\n  actions: {\r\n    increment: state => ({ ...state, value: state.value + 1 })\r\n  },\r\n  Counter: ({ state, actions }) => (\r\n    <main>\r\n      <h1>{state.value}</h1>\r\n      <button onClick={actions.increment}>+</button>\r\n    </main>\r\n  )\r\n}\r\n\r\napp({\r\n  init: counter.state,\r\n  view: state => (\r\n    <counter.Counter\r\n      state={state}\r\n      actions={{ increment: counter.actions.increment }}\r\n    />\r\n  ),\r\n  container: document.body\r\n})\r\n```\r\n\r\nWhat if we want to have two counters on the page? Our app can be as complex as we want it to be. The module exists not to simplify or reduce boilerplate, but to encapsulate the business logic of the counter. In this case, we want to encapsulate the counter's view/component and the implementation of the increment action, i.e., state + 1.\r\n\r\n```jsx\r\nconst IncrementCounterA = state => ({\r\n  ...state,\r\n  counterA: counter.actions.increment(state.counterA)\r\n})\r\nconst IncrementCounterB = state => ({\r\n  ...state,\r\n  counterB: counter.actions.increment(state.counterB)\r\n})\r\n\r\napp({\r\n  init: {\r\n    counterA: counter.state,\r\n    counterB: counter.state\r\n  },\r\n  view: state => (\r\n    <div>\r\n      <counter.Counter\r\n        state={state.counterA}\r\n        actions={{ increment: IncrementCounterA }}\r\n      />\r\n      <counter.Counter\r\n        state={state.counterB}\r\n        actions={{ increment: IncrementCounterB }}\r\n      />\r\n    </div>\r\n  ),\r\n  container: document.body\r\n})\r\n```\r\n\r\nNote that our main application has to declare new actions for incrementing CounterA and CounterB, but it doesn't know _how_ it is incremented. The application is only aware that the counter has an increment action. The counter module has no knowledge of more than a single counter nor is it able to update the user's state by itself. We need to wire this ourselves.\r\n\r\nWe can simplify this boilerplate using a function that wraps the counter action and creates a \"focused\" action. The implementation details of the `map` function are fascinating, but irrelevant here. This function need not be named `map` either. In practice, you will be able to use an existing userland solution in the form of an external library and not implement it yourself.\r\n\r\n```js\r\nconst IncrementCounterA = map({ counterA: counter.actions.increment })\r\nconst IncrementCounterB = map({ counterB: counter.actions.increment })\r\n```\r\n\r\nCan you imagine an improved version of the `map` function that takes a component and returns a _new_ component with our actions wired out of the box?\r\n\r\n```jsx\r\nconst CounterA = map(counter.Counter, { counterA: counter.actions.increment })\r\nconst CounterB = map(counter.Counter, { counterB: counter.actions.increment })\r\n\r\nconst view = state => (\r\n  <main>\r\n    <CounterA state={state.counterA} />\r\n    <CounterB state={state.counterB} />\r\n  </main>\r\n)\r\n```\r\n\r\nIf we change our module to follow some specific naming guideliens (interface), `map` usage could be simplified even further to allow something like this.\r\n\r\n```jsx\r\nconst CounterA = map({ counterA: counter })\r\nconst CounterB = map({ counterB: counter })\r\n```\r\n\r\n### Inside-out approach\r\n\r\nWIP\r\n\r\nhttps://gist.github.com/zaceno/0d7c62be81a845857e755c1378b7dbff\r\n\r\ncc @zaceno / Squirrel"},"comment":{"url":"https://api.github.com/repos/jorgebucaran/hyperapp/issues/comments/432732356","html_url":"https://github.com/jorgebucaran/hyperapp/issues/749#issuecomment-432732356","issue_url":"https://api.github.com/repos/jorgebucaran/hyperapp/issues/749","id":432732356,"node_id":"MDEyOklzc3VlQ29tbWVudDQzMjczMjM1Ng==","user":{"login":"zaceno","id":2061445,"node_id":"MDQ6VXNlcjIwNjE0NDU=","avatar_url":"https://avatars3.githubusercontent.com/u/2061445?v=4","gravatar_id":"","url":"https://api.github.com/users/zaceno","html_url":"https://github.com/zaceno","followers_url":"https://api.github.com/users/zaceno/followers","following_url":"https://api.github.com/users/zaceno/following{/other_user}","gists_url":"https://api.github.com/users/zaceno/gists{/gist_id}","starred_url":"https://api.github.com/users/zaceno/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zaceno/subscriptions","organizations_url":"https://api.github.com/users/zaceno/orgs","repos_url":"https://api.github.com/users/zaceno/repos","events_url":"https://api.github.com/users/zaceno/events{/privacy}","received_events_url":"https://api.github.com/users/zaceno/received_events","type":"User","site_admin":false},"created_at":"2018-10-24T16:35:31Z","updated_at":"2018-10-24T16:35:31Z","author_association":"CONTRIBUTOR","body":"@mshgh sure, I don’t see any reason why not. But I’m not sure I understand how you were using squirrel with init. Show us some code?"}}
{"actor":{"display_login":"zaceno"}}