jorgebucaran / hyperapp

1kB-ish JavaScript framework for building hypertext applications
MIT License
19.08k stars 780 forks source link

Async access to current state, outside actions #500

Closed zaceno closed 6 years ago

zaceno commented 6 years ago

Abstract / TL;DR; When you try to access the state asynchronously in the view, like in onbeforeremove or in setTimeout called in event handlers, the state will be stale. There are workarounds, but they are not ideal.

The long version:

If you define a function inside the view, which will be called at a later time, that function will not have access to the current state at the time it is called. If you try, the state you get will be stale. This means, for example, you cannot rely on state inside onbeforeremove handlers, or in setTimeouts called in event handlers. For a concrete demonstration of this problem, see this example of a carousel:

https://codepen.io/zaceno/pen/WdrZdK

I'm using hyperapp-transitions in order to have a not too contrived, but still short, example. All you need to know is that

1) transitions.exit runs the transition in the onbeforeremove handler. 2) The function passed to the css prop is called at the time of the transition, in this case to determine the direction to exit/enter. (Since it depends on where the user clicked)

Notice when you click the buttons of the carousel that as long as you times in a row, the new images slide in the same direction as the old images slide out. But every time you switch directions, the image leaves according to the old direction, while the new image slides in from the correct direction.

The explanation for this, is that the exit transition (and hence onbeforeremove handler, has the old state in it's closure) -- it hasn't ever even seen the new state, since it's no longer in the vtree (that's why its being removed).

There are, of course, workarounds...

One way to solve it, would be to keep an up to date copy of the relevant state somewhere, accessible by the functions that need it. That's how I solved it for the caruousel example in this codepen:

https://codepen.io/zaceno/pen/LeGQab?editors=0010

See especially lines 14, 32, 37, 38. Notice how now when you switch directions, it works as intended. This works for the occasional one off, but is obviously far from ideal.

Another solution is to use an action (since actions are special, and always have access to the current state when they're called), relying on the fact that actions return their partial state. See:

https://codepen.io/zaceno/pen/ZvQrgE?editors=0010 (notice lines 14, 16, 22)

This solution is better than the previous, but also unsatisfying (for me at least). It's annoying that simply accessing the state will cause an update and rerender.

jorgebucaran commented 6 years ago

@zaceno getState does not cause a re-render.

const actions = {
  getState: () => state => state
}
zaceno commented 6 years ago

@JorgeBucaran It doesn't? Oh is that the immutability-thing? Ok, well then it's not so bad :) (It's still not the most pleasing api, but that's ok. The main issue I had with it was that I thought it would cause the merge ti run through, and at least a call to repaint and possibly render.)

jorgebucaran commented 6 years ago

@zaceno It will not even cause a merge. When you call getState, we'll produce a state slice for the action (the correct paths to obtain it are stored inside the action closure), then we'll call the action with it. If the action returns the same slice, we'll skip both merge and repaint/render and return immediately.

okwolf commented 6 years ago

Performance optimizations for the win! 🍻

jorgebucaran commented 6 years ago

@zaceno What do you say we compromise this with getState?

zaceno commented 6 years ago

@JorgeBucaran yes I think that's the way to go for now!

jorgebucaran commented 6 years ago

👍