-
Notifications
You must be signed in to change notification settings - Fork 782
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
Better async actions support #558
Conversation
Codecov Report
@@ Coverage Diff @@
## master #558 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 1 1
Lines 136 141 +5
Branches 41 44 +3
=====================================
+ Hits 136 141 +5
Continue to review full report at Codecov.
|
@ForsakenHarmony What do you suggest we do when you depend on the current state to compute the next state, but the state you refer to inside the resolving promise has already gone stale? |
Wouldn't we have the same problem with the current way of doing things? |
You mean me? |
Nope. Because you always have a fresh copy of the state inside an action. It does mean you usually need two actions when dealing with async side-effects. |
Remember that in 0.1x it used to support that feature. |
Could still use the current way if you need the current state, not like that falls away |
Yes, we used to handle this, but it adds a lot of complexity and it was removed (among other things) for the 1.0 cut. |
If you look at the gif example it needs an extra action to set the url, which seems unnecessary, it doesn't do anything with the current state |
That one could hook HyperApp to turn async actions into function calls so that effectively we need only one action. |
Here is an example of the problem. const state = {
count: 0
}
const actions = {
down: () => state => ({ count: state.count - 1 }),
up: () => state => ({ count: state.count + 1 }),
upAsync: () => async state => {
await delay(1000)
// The `state.count` may be out of date already!!
return state.count + 1
}
}
const view = (state, actions) => (
<main>
<h1>{state.count}</h1>
<button onclick={actions.down}>-</button>
<button onclick={actions.up}>+</button>
<button onclick={actions.upAsync}>+</button>
</main>
)
app(state, actions, view, document.body) |
And here is an example of how we solved it as well: const state = {
count: 0
}
const actions = {
down: () => state => ({ count: state.count - 1 }),
up: () => state => ({ count: state.count + 1 }),
upAsync: () => async state => {
await delay(1000)
// But returning a function that expects a fresh state is OK!
return state => state.count + 1
}
}
const view = (state, actions) => (
<main>
<h1>{state.count}</h1>
<button onclick={actions.down}>-</button>
<button onclick={actions.up}>+</button>
<button onclick={actions.upAsync}>+</button>
</main>
)
app(state, actions, view, document.body) |
So since Promises and asynchronous actions are so important in any practical application you at least provide us with a way that we could add that back. |
@jorgebucaran as said before, if you need the current state you can dispatch more actions, but having to write setters for everything is a slight mess imo |
At least state fragments are usually not changed that way, practically speaking |
Maybe we can ignore the problem of the state and recommend that, if you need the current state to compute the next, you use a custom I remember @SkaterDad being in favor of this partial approach. /cc @SkaterDad @zaceno @okwolf |
But the current solution is much uglier in my opinion. |
Also, @Mytrill can you chime in? 🙏 |
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.
Maybe with a helper function?
You know, I worked a lot about this feature, along with @ngryman and @jorgebucaran. So, I am on your side regarding the fact to have to write both actions for async and reducers. I am pretty sure we can found a way to do it. #450 was personnaly my best candidate for it. |
My preference would be not to include this PR, because I much prefer the current behavior of actions simply returning the promise, so that a calling action can await whatever it resolves to, and perform further actions based on that. I can see how the proposed behavior could be convenient in some cases. But wouldn't a userland solution such as the following work almost just as well? actions: {
reduce: fn => (state, actions) => fn(state, actions),
someAction: () => (state, actions) => {
fetch('...')
.then(body => body.json())
.then(data => actions.reduce(state => ({
foo: state.foo + data.foo,
bar: data.bar,
fetching: false,
}))
return {fetching: true}
}
} |
But uglier… |
Even if you add this, you can still use that reduce function if you need the current state, I don't see why it's bad |
@ForsakenHarmony It's problematic, as exemplified here. @zaceno's solution is simple, userland and even fun to write. Your proposal is not really new, we've had it implemented before as some people already mentioned in the comments. I'll keep this open for a bit longer, as I want to gather more feedback though. |
imo that's for the user to handle, but alright |
I agree with this, 100%. Way back when Knowing that |
@jorgebucaran sorry that was just my opinion. |
And I read that again. That was awesome. I used to write an indicator jQuery plugin just for indicating that an asynchronous action has begun. Sorry for my misunderstanding of the code. |
@zaceno holy shit 🙏 |
I know I'm late to the party, but I brought you a new HOA as a housewarming gift 🎁 I call the little guy Here's a different way of writing the @zaceno example using const actions = {
someAction: () => function*(state) {
yield { fetching: true }
yield fetch(...)
.then(body => body.json())
.then(data => state => ({
fetching: false,
foo: state.foo + data.foo,
bar: data.bar
}))
}
} Now to prove that this isn't all just smoke and mirrors, I threw together an updated version of the GIF search using this newborn dude: https://codepen.io/okwolf/pen/EoeyZW?editors=0010 For myself, I'm going to skip all this complexity and stick with treating effects as data using |
Hacker! |
I am also for not merging this PR, my reason is that often, for async actions in particular, a single user interaction should trigger multiple async updates in different state slice and you want to gather all the results, and only if successful, update each slice with it's own result, so for me it is a good feature that a user can return a promise containing an object and not automatically merge this object in the current state slice. |
In frapp, you have to call the |
but if you want that, does it have to be a state action? |
Then @zaceno, your solution means that someAction might well be some non-action function: var someFunc = (reduce) => {
reduce(() => {fetching: true});
fetch('...')
.then(body => body.json())
.then(data => reduce(state => ({
foo: state.foo + data.foo,
bar: data.bar,
fetching: false
}));
}; Then just let a component pass the |
And ordinary functions could be dynamically injected just the good old way. |
Okay, I am just starting to realize @ForsakenHarmony and @infinnie are not the same person. |
👀 |
0.0 |
This allows you to return objects from async functions/promises to be merged into state the same way as you would in a sync function
This would allow you to write
instead of