-
Notifications
You must be signed in to change notification settings - Fork 780
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
Comments
In: Edit: I suppose
would do the trick. Never mind :) |
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. |
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 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. |
@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 |
@okwolf Well noted. I've improved the section that describes the signature of actions. |
@jorgebucaran do you think it's worth noting that the "two-element array" is acting as a JavaScript version of a 2-tuple? |
@jorgebucaran really nice writeup. @cryza I too have been thinking hard about 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. |
If/when a debugger shows up, it might be nice if it, in addition to looking for That way it could be possible to use action-factory patterns, like |
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. |
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} /> |
@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! |
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 Implementationconst IncrementBy = (state, { number }) => state + number Action UsageUnfolded<button
onClick={[
IncrementBy,
{
number: 5
}
]}
>
Add 5
</button> Folded<button onClick={[IncrementBy, { number: 5 }]}>Add 5</button> |
In the Slices section, where does the
|
@ddmng It comes from 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. |
OK, but I was expecting
instead of
|
@ddmng Oh, whoops! My bad. You are right. It's been corrected now. 😅 |
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 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 |
@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
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.
Glad to hear this. I hope V2 is everything you are looking for to build frontend apps! |
@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. |
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>
)
}) |
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 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. |
@zaceno would this: https://stackblitz.com/edit/ha2-insideout?file=index.js be an example of inside-out approach, with callbacks? |
@ddmng yes that's exactly what I was thinking! |
This comment has been minimized.
This comment has been minimized.
@sergey-shpak Thank you for your question. Let's discuss that in #765 if you want. 🙏 |
Inside-out approach for modules heavily depends to I tried to use this function to initialize the state too, but it failed as the |
@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? |
"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. If you need even more powerful tool with handling for undefined keys and stuff: partial.lenses |
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/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
}) |
@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. |
@ivan-kleshnin thank you for the link. Understand what you are saying. Personally, I wouldn't mind to use one-purpose simplification. |
can we change actions signature from Is there any reason to keep actions param limit except TS typing (this should be clarified)? |
Good news! The action API is now complete and available in the latest |
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.
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.
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.
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.
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.
Declarative actions
Signature
All actions use the same signature.
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.
Actions without a payload
Here is an action without a payload that increments the state by one.
Hyperapp can dispatch
Increment
in response to an event like a button click like this.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.
Hyperapp can dispatch
IncrementBy
like this.You can also curry
IncrementBy
:And use it like this.
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.
You can customize the payload using a payload creator.
If you need a custom payload with the event object and other data, you can use a payload creator too.
Event options
When dispatching actions in response to DOM events, we can tell Hyperapp to create a
passive
event handler usingaddEventListener
'soptions
under the hood. Passive event handlers are useful for improving performance on mobile devices.You can avoid clutter and improve readability using a function to create the action object:
Likewise, sometimes we need to prevent an event's default action using
event.preventDefault()
or useevent.stopPropagation()
.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.
or
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.
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.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.
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.
Here is an action that creates an HTTP request using JSX.
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.
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.
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 namedmap
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.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?If we change our module to follow some specific naming guidelines (interface),
map
usage could be simplified even further to allow something like this.Inside-out approach
@zaceno / Squirrel
https://gist.github.com/zaceno/0d7c62be81a845857e755c1378b7dbff
The text was updated successfully, but these errors were encountered: