-
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
V2 Subscriptions #752
Comments
Excited to see this. I have this idea for using Effects/Subs for game loops and theoretically it should work quite well with canvas 😂 However my first experiment will be with the |
@selfup I have a subscription that implements an animation loop using |
Oh sweet! Glad I am not alone in those thoughts. I am tethering right now (travel) so I'll check it out tomorrow 🎉 |
If |
@okwolf Good point. Subscriptions are the preferred option for interoperability now. In fact, I am not sure about returning the internal |
I would love for the internal dispatch to be exposed, but I can understand if you don't want to. Sometimes it's just easier to just call dispatch directly for that legacy code that just won't go away 😂 Thanks for bringing it up! @okwolf seems that subs look like effects after all? The constructors at least. Anyways, still stoked to see what we discover as we play around with effects/subs/action for v2 🎉 |
Just a thought but are subscription libraries and effects libraries separate or would you combine them into drivers as cyclejs does? That then makes me wonder about async if http request + response would be effect + event. Again cycle does it that way but you then need IDs to tie the 2 together. |
@SteveALee I'd like to group libraries by a common domain, e.g., @hyperapp/time should export both a @hyperapp/http should export a Here is an example. const Increment = (state, value) => state + value
const DelayIncrement = (state, { value, duration }) => [
state,
delay({ action: [Increment, value], duration })
]
app({
init: 0,
view: state => (
<main>
<h1>{state}</h1>
<button onClick={[Increment, 2]}>Add 2 Now</button>
<button onClick={[DelayIncrement, { value: 100, duration: 5000 /*ms*/ }]}>
Add 100; 5s Later
</button>
</main>
),
subscriptions: state => tick({ interval: 1000, action: [Increment, 1] }),
container: document.body
}) One more thing, aborting a fetch request is not possible with an effect. Effects model side-effects. Subscriptions model event streams. So, I imagine there could be a fetch subscription (subject to a better name) that lets you create or tap into a transfer stream which can be canceled at any time. |
Great - I think that's is best The example is great. I was wondering if I might group my actions into a object as V1 does. Purely for stylistic reasons. Was there a reason you do NOT do that? other and needing ref with Sorry for confusion re fetch. As long a Fetch effect is async that's all good (actually I need to see examples of async effects, I assume you have them). I've worked with many async comms protocols in systems dev and was really complicating the issues So an alternative style of handling Async remote request / responses rather than effect returning a promise that 'completes' on the expected response is to completely decouple them (effect are then only ever output only with no return, and subscriptions handle all input).
There is no wait of promise at all. The event just comes in. You can fire off multiple effects and then handle the events as the arrive (in any order). However you usually need to tie them together in the correct sequence (, like TCP/IP does it's packet matching), depending on the request / response protocol (perhaps multiple responses are possible for one request). So either the protocol lets you supply an ID in the request and returns it in the matching response. Or you need to handle them locally for the effect and event. Cycle takes this approach, leaving user-land to manage the IDs so the Response is connected to the request (that was just sent, so no overlap) Anyway with HTTP Req/Resp that is not necessary ASFAIK :) So ignore me |
yay! Tuples :) |
This seems to be pretty similar to Reason subscriptions, which are very helpful. https://github.com/reasonml/reason-react/blob/master/docs/subscriptions-helper.md |
Fascinating. I'm not familiar with Reason subscriptions, but you must be right @guido4000. I have mixed feelings about their API, and I am not sure where the similarities begin and end, but we're trying to accomplish the same thing and that's what matters. V2 subscriptions are based on elm's subscriptions (and tasks). Like elm, each subscription can be added and removed conditionally, but we can express them using an array right off the bat (whereas in elm you need to use Sub.batch). Unlike elm, a deeply nested batch of subscriptions can be expressed using nested arrays and any falsy elements in the array denote a subscription should be canceled. We're holding firm on the functional programming front, but also embracing JavaScript dynamic nature. Using the official effects/subscription libraries, @hyperapp/time, @hyperapp/geolocation, etc., will be the norm instead of rolling your own. Still, I designed the API as a user-facing API and not a low-level API, so creating them is a piece of cake. |
So for the purposes of interoperability, do we have any best practices to recommend? Would it make sense to have a subscription that adds a listener for Or would it be better to use some sort of small event bus? Perhaps some other approach I haven't considered yet? |
Absolutely. This is an excellent way to start events inside Hyperapp from an external source, e.g., a React app co-existing in the same document. |
👋 @sergey-shpak Please post questions in a new issue or join the Slack for better average response times. |
Corrected mistakes and other small errors. Thanks, @lukejacksonn. 😄 |
Summary
Working with traditional event emitters requires a lot of complicated resource management like adding and removing event listeners, closing connections, clearing intervals—not to mention testing asynchronous code is hard. What happens when the source you are subscribed to shuts down? How do you cancel or restart a subscription?
Subscriptions are a declarative abstraction layer for managing global events like window events and custom event streams such as clock ticks, geolocation changes, handling push notifications, and WebSockets in the browser.
Think of subscriptions as "virtual DOM meets event streams". Events are represented using plain objects called subscriptions. When the program state changes we compare the new subscriptions against the old, compute their differences and rewire the underlying connections to match the desired state.
In this issue, I'll introduce everything you need to know about the upcoming Subscriptions API.
Background
Let's start with the good news. If you know how to attach on-event listeners to UI elements, you already know how subscriptions work!
As long as the button remains in the view, Hyperapp will dispatch Increment every time the button is clicked.
In the next example, a Reset button is shown only after the Increment button is clicked once. Click it to set the count back to 0 and make the button go away.
The declarative view model allows us to add and remove elements dynamically, which in turn cancels any event listeners it was subscribed to.
Subscriptions work similarly to on-event listeners, but on a global level. Without subscriptions, we'd use the imperative DOM API:
addEventListener
,removeEventListener
,setInterval
,requestAnimationFrame
, etc., to handle global events, window events, intervals, and repaint cycle events.Subscriptions allow us to do the same in a declarative way.
Declarative subscriptions
We want to be notified when something happens in the outside world: mouse movements, keyboard presses, clock ticks, browser repaints, and so on. Subscriptions allow us to listen for such things.
A subscription is a plain object that identifies the type of the event you want to listen to, the action to dispatch with the event data and other data it needs. Not every subscription produces an Event. They can be used to model other event streams such as geolocation changes, time intervals and custom events.
To declare subscriptions we'll use the new
subscriptions
function. It receives the current state and returns one or more subscriptions. Like with effects (#750) you'll likely be using an existing subscription library to create subscriptions: @hyperapp/time, @hyperapp/mouse, @hyperapp/keyboard, etc.Hyperapp will call
subscriptions
to refresh subscriptions whenever the state changes. There you can conditionally add, update and remove subscriptions much the same way you do with elements in the view function.If a new subscription appears in the array, we'll start the subscription. When a subscription leaves the array, we'll cancel it. If any of its properties change, we'll restart it. Otherwise, we'll skip to the next subscription until we've seen them all.
Examples
SVG Clock
Countdown Clock
Try it here.
Implementing subscriptions
If you want to implement your own subscription then either one (or more) of these things are happening:
PhaserLink
and there is no Hyperapp subscription for it, so you'll need to write one.Let's implement a few subscriptions to see how it's done. There are two parts to implementing your own subscription:
The constructor function takes an object with the properties required by the subscription and returns an object that Hyperapp understands.
The implementation function encapsulates the subscription setup: adding event listeners, installing a callback, etc. It receives what the constructor gives it and is responsible for dispatching actions to Hyperapp. It returns a function that knows how to unsubscribe from your event source so that Hyperapp can cancel the subscription if it needs to.
Time ticks
Mouse moves
The text was updated successfully, but these errors were encountered: