Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2 Actions #749

Closed
jorgebucaran opened this issue Aug 27, 2018 · 35 comments
Closed

V2 Actions #749

jorgebucaran opened this issue Aug 27, 2018 · 35 comments
Labels
discussion docs You read it here first
Milestone

Comments

@jorgebucaran
Copy link
Owner

jorgebucaran commented Aug 27, 2018

V2 actions are not like V1 actions. What? They're better. In this issue, I'll talk about the new Action API in V2. My goal is to introduce the changes already in the beta release (#726) and to gather your feedback and comments.

I'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.

Summary

Let's begin by addressing the fact that V2 actions are used declaratively as opposed to V1 actions, that are used imperatively. Let's not discuss the merits of a declarative vs. imperative paradigm here or why one is better than the other (or not), but check this out if you want to know what other people say about it.

In V1, we invoke actions in the same way we invoke any other function in JavaScript. We call actions inside event listeners within the view, inside other actions or outside the view from global event listeners, etc.

const foo = () => state => partialState
const bar = value => state => partialState
const baz = value => state => partialState

const view = (state, actions) => (
  <div>
    <button onclick={() => actions.foo()}>Dispatch (No Payload)</button>
    <button onclick={() => actions.bar(42)}>Dispatch (With Payload)</button>
    <input type="text" oninput={event => actions.baz(event.target.value)} />
  </div>
)

In V2 we don't invoke actions, Hyperapp does. We assign the action to some element's on-event attribute and Hyperapp invokes it when the event is triggered. We can also tell Hyperapp to dispatch to respond to an effect or a subscription. See #750, #752.

Also, V2 actions return not a partial state, but a new state. In V1, Hyperapp shallow-merges the application state with the return value of an action. In V2, Hyperapp replaces the application state with the return value of an action.

const foo = () => state => newState
const bar = (state, value) => newState
const baz = (state, value) => newState

const view = state => (
  <div>
    <button onclick={foo}>Dispatch (No Payload)</button>
    <button onclick={[bar, 42]}>Dispatch (With Payload)</button>
    <input type="text" oninput={[baz, event => event.target.value]} />
  </div>
)

Unlike V1, actions are not "wired" to the app. There is no actions object to pass in to the app() function and the built-in "slices" feature is no longer available. I'll describe how you can still use a flavor of slices (henceforth referred to as modules) later in this issue.

In V2, actions receive the event object as the default payload if assigned to a DOM event. When working with effects and subscriptions, actions receive whatever the effect or subscriptions sends to them.

const Change = (state, event) => ({ ...state, value: event.target.value })

// ...

<input type="text" oninput={Change} />

To dispatch an action with a custom payload, we use a 2-tuple, action-payload pair that consists of the action and the custom payload.

const Engage = (state, answer) => ({ ...state, answer })

<button onclick={[Engage, 42]}>Click Here</button>

To create a custom payload with the event object (or an effect/subscription's result), we use a payload creator. A payload creator is a function that takes in the default payload of the event and returns a new payload with the data you want. A payload creator is used instead of a custom payload when dispatching an action.

const Change = (state, value) => ({ ...state, id: payload.id, value: payload.value })

// ...

<input type="text" oninput={[Change, e => ({ id: state.id, value: e.target.value })]} />

Declarative actions

Signature

All actions use the same signature.

const Action = (state, payload) => newState

Also, actions can return a 2-tuple, state-effect that consists of the new state and a plain object representation of a side effect as described later in this issue.

const ActionWithEffect = (state, payload) => [newState, effect]

// or

const ActionWithEffect = (state, payload) => [newState, [effect1, effect2, ...]]

Actions without a payload

Here is an action without a payload that increments the state by one.

The action name doesn't need to be capitalized. It's up to you.

const Increment = state => state + 1

Hyperapp can dispatch Increment in response to an event like a button click like this.

<button onClick={Increment}>Add One</button>

Notice that only onclick is valid. onClick is not.

Actions with a payload

Here is an action with a payload that increments the state by some number.

const IncrementBy = (state, number) => state + number

Hyperapp can dispatch IncrementBy like this.

<button onClick={[IncrementBy, 42]}>Add forty-two</button>

You can also curry IncrementBy:

const IncrementBy = numer => state => state + number

And use it like this.

<button onClick={IncrementBy(42)}>Add forty-two</button>

The downside is that IncrementBy(42) returns an anonymous function. Because anonymous functions don't have a name, a debugger will not be able to find out the name of the action.

Payload creators

Actions receive the event object as a default payload when no custom payload is specified.

const IncrementBy = (state, event) => state + event.clientX

You can customize the payload using a payload creator.

const clientX = event => event.clientX
const IncrementBy = (state, x) => state + x

<button onClick={[IncrementBy, clientX]}>Add More</button>

If you need a custom payload with the event object and other data, you can use a payload creator too.

const clientXY = event => ({ x: event.clientX, y: event.clientY })
const IncrementBy = (state, x) => state + x

<button onClick={[IncrementBy, e => ({ number: state.number, ...clientXY(state) })]}>
  Add More
</button>

Event options

When dispatching actions in response to DOM events, we can tell Hyperapp to create a passive event handler using addEventListener's options under the hood. Passive event handlers are useful for improving performance on mobile devices.

<div onscroll={{ action: Scroll, passive: true }}>...</div>

You can avoid clutter and improve readability using a function to create the action object:

const passive = action => ({ action, passive: true })

// ...

<div onscroll={passive(Scroll)}>...</div>

Likewise, sometimes we need to prevent an event's default action using event.preventDefault() or use event.stopPropagation().

const preventDefault = props => ({ action: props.action, preventDefault: true })

// ...

<form onsubmit={preventDefault(SubmitForm)}>
    <label>
        Name:
        <input
        type="text"
        value={state.value}
        oninput={[Change, targetValue])}
        />
    </label>
    <h1>{state.value}</h1>
    <input type="submit" value="Submit" />
</form>

Updating the state

In 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 with the application state, but you can do it yourself.

const Increment = state => ({ ...state, value: state.value + 1 })

or

const Increment = state => Object.assign({}, state, { value: state.value + 1 })

Creating side effects

This section explains how effects and actions play together, not how to implement effects. There's a dedicated issue for that if you are interested. For those familiar with Elm, we call "effects" what they call "commands".

We use a declarative API to tell Hyperapp to dispatch effects. No callbacks, promises, observables, channels, async or await. Those are powerful ideas, but we don't need them today.

Effects are usually, but not limited to, asynchronous operations. Here are some examples: creating HTTP requests, creating time delays, generating random numbers, pushing or replacing a URL to/from Window.history, writing and reading to/from local/session storage, opening/closing a database connection as well as writing and reading to it using IndexedDB, requesting/exiting fullscreen, enqueing data over a WebSocket, etc.

Effects are plain objects. Much the same way a virtual node describes a DOM node, an effect describes a side effect. Hyperapp runs the effect and usually calls a designated action with the result when it's done. To create side effects, we return a 2-tuple, state-effect pair from an action.

const MakeSomeNoise = state => [newState, nextEffect]

Hyperapp will use the first element of the array to update your application state and the second element to run the effect. The effect object encapsulates the side effect. While you can create your own effects, you'll be normally importing them from official Hyperapp packages like @hyperapp/http or @hyperapp/random.

Effect objects are usually created through a function, e.g., Http.fetch, Random.generate. Here is an example that generates a random number.

import * as Random from "@hyperapp/random"

const NewFace = (state, newFace) => ({
  ...state,
  dieFace: Math.floor(newFace)
})

const Roll = state => [
  state,
  Random.generate({ min: 1, max: 10, action: NewFace })
]

app({
  init: Roll({ dieFace: 1 }),
  view: state => (
    <div>
      <h1>{state.dieFace}</h1>
      <button onClick={Roll}>Roll</button>
    </div>
  ),
  container: document.body
})

Here is an action that creates an HTTP request. Maybe it uses the fetch API under the hood and if not available, defaults to xmlhttprequest. It doesn't matter to us.

import * as Http from "@hyperapp/http"

const SendHttp = state => [
  { ...state, error: null, fetching: true },
  Http.fetch({
    url: state.url,
    action: SuccessResponse,
    error: ErrorResponse
  })
]

Because the effect API is compatible with Hyperapp's pure component interface, it's possible to use JSX to describe them. Doing that or not is up to you. I'm just letting you know you can. Here is an action that generates a random number, but this time using JSX.

import * as Random from "@hyperapp/random"

const Roll = state => [
  state,
  <Random.generate min={1} max={10} action={NewFace} />
]

Here is an action that creates an HTTP request using JSX.

import * as Http from "@hyperapp/http"

const SendHttp = state => [
  { ...state, error: null, fetching: true },
  <Http.fetch url={state.url} action={SuccessAction} error={ErrorAction} />
]

An action can produce an effect that notifies an action that produces another effect and so on, thus creating an effect series.

Modules

We 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.

V2 has no built-in mechanism to create object state namespaces, or slices, as they were called in V1, but that doesn't mean we can't replicate that functionality in V2 as I'll explain in this section.

Bottom-up approach

This section describes a bottom-up approach to building modules. You start with a 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.

Here is how you can wire a module to your application.

const counter = {
  state: { value: 0 },
  actions: {
    increment: state => ({ ...state, value: state.value + 1 })
  },
  Counter: ({ state, actions }) => (
    <main>
      <h1>{state.value}</h1>
      <button onClick={actions.increment}>+</button>
    </main>
  )
}

app({
  init: counter.state,
  view: state => (
    <counter.Counter
      state={state}
      actions={{ increment: counter.actions.increment }}
    />
  ),
  container: document.body
})

What if we want to have two counters on the page? Our app can be as involved 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.

const IncrementCounterA = state => ({
  ...state,
  counterA: counter.actions.increment(state.counterA)
})
const IncrementCounterB = state => ({
  ...state,
  counterB: counter.actions.increment(state.counterB)
})

app({
  init: {
    counterA: counter.state,
    counterB: counter.state
  },
  view: state => (
    <div>
      <counter.Counter
        state={state.counterA}
        actions={{ increment: IncrementCounterA }}
      />
      <counter.Counter
        state={state.counterB}
        actions={{ increment: IncrementCounterB }}
      />
    </div>
  ),
  container: document.body
})

Note that our main application has to declare new actions for incrementing CounterA and CounterB, but it doesn't know how it's done. The application is only aware that the counter has an increment action. The counter module doesn't know about multiple counters or is it able to update the user's state by itself.

We can simplify this boilerplate using a function that wraps the counter's 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.

const IncrementCounterA = map({ counterA: counter.actions.increment })
const IncrementCounterB = map({ counterB: counter.actions.increment })

Can 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?

const CounterA = map(counter.Counter, { counterA: counter.actions.increment })
const CounterB = map(counter.Counter, { counterB: counter.actions.increment })

const view = state => (
  <main>
    <CounterA state={state.counterA} />
    <CounterB state={state.counterB} />
  </main>
)

If we change our module to follow some specific naming guidelines (interface), map usage could be simplified even further to allow something like this.

const CounterA = map({ counterA: counter })
const CounterB = map({ counterB: counter })

Inside-out approach

@zaceno / Squirrel

https://gist.github.com/zaceno/0d7c62be81a845857e755c1378b7dbff

@zaceno
Copy link
Contributor

zaceno commented Aug 27, 2018

In: const IncrementCounterA = map({ counterA: counter.actions.increment }), is it possible to implement map in such a way that IncrementCounterA.name === "IncrementCounterA" ? (or something else equally meaninful?)

Edit:

I suppose

const IncrementCounterA = state => map({counterA: counter.actions.increment})(state)

would do the trick. Never mind :)

@okwolf
Copy link
Contributor

okwolf commented Aug 27, 2018

Great job! I think the only thing I see missing is what actions are allowed to return. There’s some examples but not a complete enumeration.

@manu-unter
Copy link

manu-unter commented Aug 27, 2018

Nice and lean API!

One thing I haven't quite found the reasoning behind is your decision to go with a second, mapped parameter for parameterized actions instead of currying. Maybe you can elaborate on that?

To be more exact: I would like to find out why you chose to do

const IncrementBy = (state, { number }) => state + number 

// and 

onClick={{ action: IncrementBy, number: 5 }}

instead of

const IncrementBy = number => state => state + number

// and 

onClick={IncrementBy(5)}

@SteveALee
Copy link

  • How do you use side effects? Can you add an example line of code?
  • When is the DOM updated (view invoked) and is it not invoked if a side effect is returned?

@jorgebucaran
Copy link
Owner Author

@SteveALee I improved the section that explains side effects as they are related to actions. I'll create a new dedicated issue to discuss effects later and crosslink it here.

@jorgebucaran
Copy link
Owner Author

@cryza You can do that too if you want and Hyperapp won't (and can't) prevent you from doing it. The disadvantage is poor debugability. The curried IncrementBy returns an anonymous function without a name, i.e., function.name is undefined. This means that an extension to Hyperapp adding debug features like Elm's time traveler or logger would suffer or be unable to function correctly.

@jorgebucaran
Copy link
Owner Author

@okwolf Well noted. I've improved the section that describes the signature of actions.

@okwolf
Copy link
Contributor

okwolf commented Aug 28, 2018

@jorgebucaran do you think it's worth noting that the "two-element array" is acting as a JavaScript version of a 2-tuple?

@lukejacksonn
Copy link
Contributor

@jorgebucaran really nice writeup.

@cryza I too have been thinking hard about props => state => newState and @jorgebucaran justification; the disadvantage is poor debugability.

Assuming this is the only disadvantage then you could get around this by doing:

var IncrementBy = number => function Increment (state) {
  return state + number
}

Not ideal but could be done if you were having trouble or fear debugging an anonymous action, although AFAIK we don't have a demonstrable debugger for V2 yet.

@zaceno
Copy link
Contributor

zaceno commented Aug 28, 2018

If/when a debugger shows up, it might be nice if it, in addition to looking for .name first looked for something like ._name or ._debug_name

That way it could be possible to use action-factory patterns, like combineActions, or scopedAction('foo.counter', x => x +1) and attach a name to it for the debugger, post-definition.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Aug 28, 2018

I forgot to mention another disadvantage of the suggested curried action approach.

Because it creates a new function, the algorithm responsible for diffing props will never detect that one is using the same action when only its input/arguments have changed. This means we will touch the DOM when diffing event handlers when we could have avoided to do so.

@lukejacksonn
Copy link
Contributor

lukejacksonn commented Aug 28, 2018

The workaround for this would be to declare your curried function upfront, like so:

const IncrementBy = number => state => state + number
const IncrementBy5 = IncrementBy(5)

// then

<button onclick={IncrementBy5} />

@jorgebucaran
Copy link
Owner Author

workaround — a method for overcoming a problem or limitation in a program or system.

@lukejacksonn Because there is a canonical way of doing this using the parametreized event signature, I wouldn't call that a workaround (which can be easily misinterpreted), but an alternative to the official way. But, yes, that should work!

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Aug 28, 2018

It is worth mentioning that there is/was an alternative proposal to the parameterized action API that uses an array as a tuple instead of an object like this.

Action Implementation
const IncrementBy = (state, { number }) => state + number
Action Usage
Unfolded
<button
  onClick={[
    IncrementBy,
    {
      number: 5
    }
  ]}
>
  Add 5
</button>
Folded
<button onClick={[IncrementBy, { number: 5 }]}>Add 5</button>

@okwolf okwolf added the docs You read it here first label Aug 29, 2018
@ddmng
Copy link

ddmng commented Aug 29, 2018

In the Slices section, where does the actions.onClick come from?

      <button onClick={actions.onClick}>+</button>

@jorgebucaran
Copy link
Owner Author

@ddmng It comes from actions={{ increment: counter.actions.increment }} when the Counter component is created in the main view.

app({
  init: counter.state,
  view: state => (
    <counter.Counter
      state={state}
      actions={{ increment: counter.actions.increment }}
    />
  ),
  container: document.body
})

Keep in mind that this is just how I chose to write this particular piece of code. Hyperapp doesn't force you to adopt this style. For example, some people may prefer to export the state, actions, and components separately.

@ddmng
Copy link

ddmng commented Aug 29, 2018

OK, but I was expecting

<button onClick={actions.increment}>+</button>

instead of

<button onClick={actions.onClick}>+</button>

@jorgebucaran
Copy link
Owner Author

@ddmng Oh, whoops! My bad. You are right. It's been corrected now. 😅

@jdh-invicara
Copy link

The modules section took a while to sink in but is exciting to me as it seems to improve encapsulation of state (if I'm understanding things correctly) and has a wonderfully straightforward implementation. However, the discussions of map left me confused. On the one hand I don't mind not understanding the implementation of map if it's provided as some form of helper function with V2 - but it sounds more like "the implementation of map is left up to the user? And to be honest, I've completely no idea how I'd do that... Perhaps a little more info in this section?

Small point: I much prefer "modules" to "slices" and I expect many developers would wonder "what's a slice?" but we immediately grok what a module is. IMHO, I'd title that section "Modules" and lead in with something along the lines of "v1 didn't have independent modules, they had something called slices...". Just kind of switch around the focus.

FYI - I've been happily using v1 for lots of small sized projects and find it a joy to use. At first I was concerned about V2 being not backwards compatible. But, personally, I don't think I'd have grokked this new V2 nearly as well if I hadn't used V1. Just my 2 cents worth.

And I'll close by saying "Fantastic work!" to everyone involved

@jorgebucaran
Copy link
Owner Author

@jdh-invicara I've taken in your suggestion and renamed the slices section to modules and added a note to explain we're talking about what in V1 slang we refer to as slices. Also added is a note clarifying details surrounding the implementation and usage of map.

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.

Finally, I've decided to split up the modules section into subsections, each one explaining a different approach to modularization in Hyperapp.

The one already described is a bottom-up-like approach so I named it thusly. There's already a placeholder for a more dynamic approach contributed by @zaceno which I've named "inside-out" for the time being. I left some links for now.

I'll continue to update this issue with new approaches as I encounter them.


I've been happily using v1 for lots of small sized projects and find it a joy to use.
I don't think I'd have grokked this new V2 nearly as well if I hadn't used V1. Just my 2 cents worth.

Glad to hear this. I hope V2 is everything you are looking for to build frontend apps!

@zaceno
Copy link
Contributor

zaceno commented Sep 1, 2018

@jdh-invicara The gist @jorgebucaran linked to under the "inside-out" heading offers one basic map-implementation.

Leaving it to userland is probably a good idea at this stage, because there are a couple different approaches to modularization (no clear winner for everyone yet), and depending on your preferred approach, you may want your map function to work different ways.

@zaceno
Copy link
Contributor

zaceno commented Sep 1, 2018

To explain what @jorgebucaran meant by the "inside out" approach, I believe this is it:

Instead of mapping the child modules actions in the parent module (and feeding them back as props), we pass a map function to the child module and let it map its own actions.

So, something like this:

counter.js

export default map => {
  const initial = 0
  const increment = map(x => x +1)
  const decrement = map(x => x - 1)
  const view = ({state}) => (
    <p>
      <button onclick={decrement}>-</button>
      {state}
     <button onclick={increment}>+</button>
  </p>
  )
  return {initial, view}
}

index.js

import Counter from './counter.js'

const counter1 = Counter(makeMap('c1'))
const counter2 = Counter(makeMap('c2'))

app({
  init: {
    c1: counter1.initial,
    c2: counter2.initial,
  },
  view: state => (
    <main>
      <counter1.view state={state.c1} />
      <counter2.view state={state.c2} />
   </main>
  )
})

@zaceno
Copy link
Contributor

zaceno commented Sep 1, 2018

The good thing about inside-out is that it's easier to add new actions to a module (you don't need to refactor in all modules that consume the one you're adding to)

On the other hand, the bottom up approach allows you to compose actions from different modules.

So for example. could make it so that a counter's increment action not only increments the value but also sets tooHigh: true in some other part of the state if the counter's value exceeds a maximum.

Edit:

This comparison is probably not valid. Even with the "inside out" pattern you could pass in additional "callback" actions to the module, and leave it up to it to compose them.

const counter1 = Counter(makeMap('c1'), {onExceedMax: setTooHighWarning})
const counter1 = Counter(makeMap('c2'), {onExceedMax: setTooHighWarning})

... it just comes down to personal preference.

@ddmng
Copy link

ddmng commented Sep 3, 2018

@zaceno would this: https://stackblitz.com/edit/ha2-insideout?file=index.js be an example of inside-out approach, with callbacks?

@zaceno
Copy link
Contributor

zaceno commented Sep 3, 2018

@ddmng yes that's exactly what I was thinking!

This was referenced Sep 7, 2018
@sergey-shpak

This comment has been minimized.

@jorgebucaran jorgebucaran added this to the V2 milestone Oct 3, 2018
@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Oct 4, 2018

@sergey-shpak Thank you for your question. Let's discuss that in #765 if you want. 🙏

@mshgh
Copy link

mshgh commented Oct 24, 2018

Inside-out approach for modules heavily depends to squirrel() function so I guess this is good place for this question.

I tried to use this function to initialize the state too, but it failed as the path provided doesn't exists. I am curious, can the squirrel() function be updated to auto-create empty objects along the path if particular key doesn't exist?

@zaceno
Copy link
Contributor

zaceno commented Oct 24, 2018

@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?

@ivan-kleshnin
Copy link

ivan-kleshnin commented Oct 25, 2018

"squirrel" function is a fancy-named lensing with some key features missed.

import * as R from "@paqmind/ramda" // R.lens, R.over + over2 

R.over2(['foo'], R.inc, {foo: 1, bar:1}) 
// => {foo: 2, bar: 1}

// Note: array path syntax allows keys with dots (and brackets...)

R.over2(['foo', 'bar', 0], R.add(2), {foo: {bar: [1, 1, 1], baz: 1}})
// => {foo: {bar: [3, 1, 1], baz: 1}}

Composition rules for lenses are mathematically researched for a long time already.
I'd suggest to not reinvent the wheel here ;)

If you need even more powerful tool with handling for undefined keys and stuff: partial.lenses

@mshgh
Copy link

mshgh commented Oct 25, 2018

Sure. I am trying to think about approach regarding state: comes first; is modular; is reusable; is separated from UI. In the code fragments lib/... means generic reusable blocks and app/... per application adoption. note: imports do not have real paths, just hint lib vs. app area
It is thoughts-in-progress stage ;)

// lib/state-slices/counting.js - the state and business logic separation from Counter
export default map => {
  const initialState = { count: 1 };
  const init = () => map(_ => initialState);
  const inc = () => map(state => ({ ...state, count: state.count + 1 }));
}

// lib/components/counter.js - the visual presentation of Counter abstracted from state/actions
export default ({title, count, onInc}) => {...some html...}

// app/slices.js - (re)use slice(s) from lib
import counting from 'lib/state-slices/counting'
export default {
  bananas: counting(squirrel('bananas')),
  apples: counting(squirrel('apples')),
}

// app/two-counters.js - connect components to state and actions and define layout
import slices from 'app/slices'
import Counter from 'lib/components/counter'
export default state =>
  h('div', {}, [
    Counter({title:'Apples', count: state.apples.count, onInc: slices.apples.inc}),
    Counter({title:'Bananas', count: state.bananas.count, onInc: slices.bananas.inc}),
  ]);

// app/index.js
import slices from 'app/slices'
import TwoCounters from 'app/two-counters'
let init = {foo: 'bar'};
// *** this is the place where squirrel() fails (...and will be loop later)
init = slices.apples.init()(init);
init = slices.bananas.init()(init);
app({
  init,
  view: state => TwoCounters(state),
  container: document.body
})

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Oct 25, 2018

@mshgh Please don't post more code (it makes this issue harder to understand for other people). 🙏

Create a new issue if you want to discuss something specific. Thanks.

@mshgh
Copy link

mshgh commented Oct 25, 2018

@ivan-kleshnin thank you for the link. Understand what you are saying. Personally, I wouldn't mind to use one-purpose simplification.
@jorgebucaran got it. Thanks for collapsing the code, didn't know this was possible.

@jorgebucaran jorgebucaran mentioned this issue Nov 13, 2018
14 tasks
@jorgebucaran jorgebucaran pinned this issue Dec 16, 2018
@sergey-shpak
Copy link
Contributor

sergey-shpak commented Jan 11, 2019

can we change actions signature from
(state[, props], data) => newState to (state[, props, ..., propsN], data) => newState ?

Is there any reason to keep actions param limit except TS typing (this should be clarified)?
@jorgebucaran @frenzzy @zaceno

@jorgebucaran jorgebucaran changed the title V2 Action API V2 Actions Mar 6, 2019
@jorgebucaran
Copy link
Owner Author

Good news! The action API is now complete and available in the latest hyperapp@beta version. I don't see it changing anymore. I've also updated this issue with the latest information so do take another look if you're new to V2's action API. 👋😄

Repository owner locked as resolved and limited conversation to collaborators May 3, 2019
@jorgebucaran jorgebucaran unpinned this issue Jul 17, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
discussion docs You read it here first
Projects
None yet
Development

No branches or pull requests