-
Notifications
You must be signed in to change notification settings - Fork 781
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
Refactor events, lifecycle, no emit return, 💯 thunks, cut >100 bytes #317
Conversation
569bb5a
to
2ee8790
Compare
Codecov Report
@@ Coverage Diff @@
## master #317 +/- ##
========================================
+ Coverage 99.37% 100% +0.62%
========================================
Files 2 2
Lines 160 146 -14
Branches 51 44 -7
========================================
- Hits 159 146 -13
+ Misses 1 0 -1
Continue to review full report at Codecov.
|
Do you remember, i was talking about that from the beginning? :)
Don't like that if i understand it correctly. Didn't see it in the diff. can you show short example? As about changing one more time the name of the app's argument... 😆 Incoming isn't "props" in any sense. |
Yup. Around here. 👈 |
@charlike I wasn't sure on Remove built-in promise support in favor of Action: Where |
Yeah, calling it "thunk composition" is confusing. It does seem like a more powerful feature, though. The built-in promise support really only handled the "happy path", right? With these thunks you can more easily do state updates from Promise errors, too? Also a fan of enforcing state to be an object 👍 |
@SkaterDad @lukejacksonn That works, but the beauty is that if you want "native" promise support like we had, you can still totally have it, not shown in your example, but in the following: events: {
resolve(state, actions, result) {
return result && typeof result.then === "function"
? update => result.then(update)
: result
}
} Certainly, you'll want to wrap that into a mixin, or better, group it with other things like support for observables or generators (I don't know) into a nice preset. |
Using For a sync action returning a partial state, the internal code will be the following : const result = action(...)
update(result) But, for an action returning a function, the internal code will be the following : const result = action(...)
result(update) Now, you understand that we are giving to the action result the internal By this way, it will be available to you to resolve to an update of the state whatever/whenever you want. (state, actions) => update => PROMISE.then(x => update(x)) (state, actions) => update => OBSERVABLE.subscribe({ next: x => update(x) }) (state, actions) => update => CALLBACK_FN(x => update(x)) |
For the record, in case anyone is now confused. You may not need to use thunks. Other than promises, whatever you were doing before will still work. For promises you'll have to use the following pattern: myAction(state,actions,data) => update =>
fetch('/users').then(user => update({ user })) Or if you do like I showed in this example, then you can do this instead: myAction(state,actions,data) => fetch('/users').then(user => ({ user })) and that'd work too. |
@jbucaran oh yea, examples clears things. It now reminds me the Based on that i prototyped some ideas about that signatures of effects, reducers, render and hooks to be the same. foo({
// `state` here would be empty object or undefined (not sure),
// since they should not have access to it
effects: {
bar: ({ actions }, arg1, arg2) => {}
},
// forced synchronous actions
reducers: {
qux: ({ state, actions }, arg1, arg2) => {}
},
// here actions and state are actually "props",
// so probably easy mocking. When stateless components
// the `state` will be empty object, for no errors
render: ({ state, actions, ...props }) => {},
hooks: {
init: ({ state, actions }) => {},
error: ({ state, actions }, err) => {}
}
}) I don't know if i'm the only one that i see one pretty fantastic ability of the destruction: allows enforcing conventions. Anyway, above is just a concept of what i like. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick review but this is some things that startled me by reading the diff.
docs/actions.md
Outdated
@@ -1,182 +1,117 @@ | |||
# Actions | |||
|
|||
Use [actions](/docs/api.md#actions) to update the state. | |||
Actions are functions which take the current [state](/docs/state.md) and return a partial state or a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves to a partial state. Actions represent the intent to manipulate the state tree. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So is promise resolving will be part of the core or wont ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The answer is "won't", but i believe it isn't the correct question. This description isn't very clear, because it don't mention that you may return a function which gets one argument called update
which may/should be used to update the state, and also that update
function returns the merged new object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, haven't updated it yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actions represent the intent to manipulate the state tree.
I still disagree for apps that are more complex than a hello world. I very often use them to e.g. communicate via socket.io
/postMessage
/http
/etc..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dodekeract I'll mention that actions have others uses too.
docs/api.md
Outdated
@@ -95,11 +97,20 @@ See also [Actions](/docs/actions.md). | |||
|
|||
#### ActionResult | |||
|
|||
A partial state or [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves to a partial state. | |||
A partial state, [thunk](#thunk) or [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves to a partial state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now ActionResult
can be whatever you want as the resolve
event allows you to transform it.
Or you are telling about ActionResult
is the only thing the core will understand do trigger an update of the state ?
@@ -85,13 +95,17 @@ app({ | |||
The `emit` function is available as the return value of the [`app`](/docs/api.md#app) function call itself. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not the truth at all, isnt it ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hence wip 😆
docs/mixins.md
Outdated
@@ -43,7 +44,7 @@ const ActionPerformance = (ignore = [], cache = []) => emit => ({ | |||
afterAction() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use resolve
instead.
src/app.js
Outdated
repaint(emit("init")) | ||
|
||
return emit | ||
return emit("load") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not :
return emit("load", emit)
To return emit by default for non breaking change here. And emit will be given to load
as data to permit to return emit even if you are returning something else.
loads(state, actions, emit) {
return { actions, emit }
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! Please! Awesome idea @Swizz !
473c819
to
3f7321d
Compare
For the first time 💯% coverage and 141B cut. 🔪 |
3f7321d
to
e6be7a4
Compare
this PR is amazing! ❤️ LGTM |
409a807
to
a4c0e01
Compare
aa35a78
to
6b18681
Compare
Wait why are we getting rid of emit? 🤔 I don't know if I like getting rid of such a powerful feature 😭 This will make it harder for people to trickle into existing apps. What is the new alternative? Are we at least returning something? Ok it seems that we are able to return whatever we want in |
@jbucaran Will we still support |
Sure, |
@selfup Sorry for the confusion, that commit message is a bit outdated, thanks for pointing it out. We'll continue to return |
@jbucaran Ok thanks for clarifying! 😄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Read through all the doc changes, I love it! Just a few nitpicks, and question about source.
src/app.js
Outdated
var loaded = false | ||
var root = props.root | ||
var host = (root && root.parentNode) || document.body | ||
var lock = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we just do var lock
with no false
? undefined
will coerce to true
, and it will just cycle between the booleans from there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me see if that works.
docs/actions.md
Outdated
} | ||
Game: gameActions, | ||
Score: scoreActions, | ||
...userActions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another nitpick, but since when has it been convention to capitalize these? If you were to ask me it just seems odd: actions.Game.tick
, actions.Score.add
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jamen There's no convention, it's just my current personal preference (but I can change my mind). What do you think is best and how can we find a way to agree on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbucaran If there is some rationale behind it, it would be nice to include a note. Previously I just remember people writing them plainly: actions.game.tick
, state.score.count
, etc. and I had no major problems with this.
It does create a nice immediate distinction between your own stuff and stuff mixins bring in, but it is not really conveyed clearly in this example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dodekeract How do you feel about this point?
docs/actions.md
Outdated
up: ({ count, maxCount }) => ({ | ||
count: count + (maxCount > count ? 1 : -maxCount) | ||
}) | ||
setText: (state, actions, text) => ({ text }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a tiny nitpick how this is written. It feels inconsistent with everything below being written as:
setText(state, actions, text) {
return { text }
}
Does/can prettier have a preference? Or should they just coexist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jamen Will fix it right away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jamen Prettier doesn't have a preference here. It'll use either version, depending on what the original code was.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be tempted to be real terse with this..
setText(state, actions, data) {
return { text: data }
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some minor things to fix in the docs. Overall I'm on board with the concepts, but there's too much changing to dig into all the details.
docs/events.md
Outdated
|
||
### afterAction | ||
- Allow actions to return a [Promise](https://github.com/tc39/proposal-observable). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
url is wrong. Points to observable docs instead of promise docs
docs/lifecycle-events.md
Outdated
## oninsert | ||
|
||
The oninsert event is fired after the element is created and inserted into the DOM. Use this event to wrap third party libraries that require a reference to a DOM node, etc. | ||
The [oncreate](/docs/api.md#oncreate) event is fired after the element is created, but before it is attached to the DOM. Use it to start animations before an element is rendered, [adapt an external library](#adapting-an-external-library) that requires a reference to a DOM node, etc. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're changing this right? It's going to be called after element has been attached to the dom, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zaceno Correct. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just tried looking through all the doc changes to see if there was anything that stood out as incorrect or could be reworded. Nothing too major!
docs/CONTRIBUTING.md
Outdated
* **Be nice.** | ||
* **Be welcoming**: We strive to be a community that welcomes and supports people of **all backgrounds and identities**. | ||
* **Be considerate**: Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. | ||
* **Be respectful**: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Double space here
docs/CONTRIBUTING.md
Outdated
|
||
- Feel free to create a new `test/*.test.js` file if none of the existing test files suits your test case. | ||
|
||
- Tests usually create an application using the [app](/docs/api.md#app) function and then check for `document.body.innerHTML` to match some expected string. The app call is async, so we often use the [patch](/docs/api.md#patch) event to detect when the view has been synthesized into an element. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this still hold true? Did we come to a conclusion on removing the patch
event or not?
docs/actions.md
Outdated
@@ -1,182 +1,129 @@ | |||
# Actions | |||
|
|||
Use [actions](/docs/api.md#actions) to update the state. | |||
Actions are functions which take the current [state](/docs/state.md) and return a partial state or a [thunk](#thunks). Actions represent the intent to manipulate the state tree. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested rewording..
Actions are functions that take some payload data and return a partial state or a thunk. They are the only way to update the state tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this link to thunks correct? Or should it be Thunks
docs/actions.md
Outdated
up: ({ count, maxCount }) => ({ | ||
count: count + (maxCount > count ? 1 : -maxCount) | ||
}) | ||
setText: (state, actions, text) => ({ text }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be tempted to be real terse with this..
setText(state, actions, data) {
return { text: data }
}
docs/actions.md
Outdated
} | ||
}) | ||
``` | ||
Actions are often called as a result of user events triggered on the [view](/docs/view.md) or inside application [events](/docs/events.md). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
triggered from a view or from inside of an application event.
}), | ||
upLater: (state, actions) => { | ||
setTimeout(actions.up, 1000) | ||
defer(state, actions, data) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how come we are using this syntax here.. and not
defer: (state, actions, data) => update => {
...
update(newData)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I see we are using this syntax everywhere..
docs/actions.md
Outdated
} | ||
}) | ||
``` | ||
|
||
Actions can be written as [async functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) too. | ||
Actions need not have a return value at all. You can use them to create side effects, call other actions, etc. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps worth mentioning explicitly that..
Actions that return nothing will not trigger a state update
docs/actions.md
Outdated
|
||
[Try it online](https://codepen.io/hyperapp/pen/Zygvbg?editors=0010) | ||
We iterate over your action keys, allowing for namespaced actions. Namespaces help you organize your actions into categories or domains. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hyperapp iterates over action keys recursively during setup, this means that actions can be nested under developer defined namespaces.
docs/events.md
Outdated
|
||
### Interoperatiblity | ||
### Interop |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not convinced about this title 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lukejacksonn I thought it sounded okay. What else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure.. perhaps just Interoperatiblity
@@ -7,7 +7,10 @@ | |||
"module": "src/index.js", | |||
"license": "MIT", | |||
"repository": "hyperapp/hyperapp", | |||
"files": ["src", "dist"], | |||
"files": [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this key used for (just out of curiosity)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It determines which files & folders get published to npm
.
https://docs.npmjs.com/files/package.json#files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SkaterDad thanks!
e96c330
to
97f67fb
Compare
- Refactor thunks. Remove built-in promise support in favor of using thunk composition. - Simplify the merge function and enforce the use of a POJO (plain old JavaScript object) as your state property. This means primitive types are no longer valid states. - Make lifecycle more consistent. → load (→ render → action → resolve → update) load: Use it to initialize and "load" your app resources here. This event fires before the very first render and actions you call here will be scheduled for render appropriately. render: Use it to overwrite the view function to "render" your own view. action: Use it to log or save information about actions. resolve: Use it to modify how actions work and what types they are allowed to return. update: Use it to inspect / validate the previous state against the future (new) state. If you return a new state inside this event, it will be used instead to update the current state tree. - Now onupdate is now called every time we patch an element. The function receives the element and the old data. This is a good place to write logic in response to prop changes. - More contributing under /docs. - Rewrite CoC based on the open code of conduct: http://todogroup.org/opencodeofconduct. - Remove rollup.config.js file, until now used only to specify the uglify plugin and use uglify on the command line directly.
97f67fb
to
9431cdc
Compare
Thank you everyone for helping to review this PR! Finally merging! 🎉 |
* Rename mixin composition to presets. * Rewrite tests. * Docs, tests, and other chores. * Refactor app.js, make variable names more consistent. - Refactor thunks. Remove built-in promise support in favor of using thunk composition. - Simplify the merge function and enforce the use of a POJO (plain old JavaScript object) as your state property. This means primitive types are no longer a valid value for `state`. - Make lifecycle more consistent. → load (→ render → action → resolve → update) load: Use it to initialize and "load" your app resources here. This event fires before the very first render and actions you call here will be scheduled for render appropriately. render: Use it to overwrite the view function to "render" your own view. action: Use it to log or save information about actions. resolve: Use it to modify how actions work and what types they are allowed to return. update: Use it to inspect / validate the previous state against the future (new) state. If you return a new state inside this event, it will be used instead to update the current state tree. - oncreate is now called after the element is created and attached to the DOM. - onupdate is now called every time we patch an element. The function receives the element and the old data. This is a good place to write logic in response to prop changes. - Both oncreate and onupdate are now invoked after patch returns. - Rewrite CoC based on the open code of conduct: http://todogroup.org/opencodeofconduct. - Remove rollup.config.js file, until now used only to specify the uglify plugin and use uglify on the command line directly.
Work in progress.