Side effects are the bread and butter of our programs. It is almost impossible to get anything useful done without them. At the same time, we want our programs to be correct and easy to test, refactor, parallelize, optimize and so on. The key is to minimize and isolate side effects, pushing them to the fringes of our code.
V2 introduces a new declarative approach to creating side effects known as _managed effects_ or just effects. Effects tell Hyperapp how to make HTTP requests, update our browser history, send data over WebSockets, etc. Effects save you from dealing with asynchronous control flow and managing side effects on your own.
In this issue, I'll introduce the upcoming Effects API. I'll explain what effects are and how to create, use and implement them.
Just like a virtual node represents a DOM element, but it is _not_ a real DOM element, effects represent the potential to produce one or more side effects but don't run any code themselves.
Think of effects as virtual commands waiting to be fulfilled. Creating them won't change the environment or involve using async functions, promises or callbacks. Effects are plain objects that carry what needs to happen along with whatever data they need.
Most of the time we'll use an effect library to create effects that Hyperapp can make sense of: @hyperapp/http, @hyperapp/random, @hyperapp/time, etc.
import * as Random from "@hyperapp/random"
const effect = Random.generate({
action: UpdateNumber,
min: 1,
max: 100
})
We'll hand the effect object over to Hyperapp as part of the return value of an action. The runtime will pick it up, execute it, and notify our application with the result when it's done.
Here are some concrete examples: creating HTTP requests, creating time delays, setting or removing the focus on a DOM element, generating random numbers, changing the browser 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.
We create effects with an effect constructor. You can implement your own effects, but most of the time you won't need to, as Hyperapp ships with all the effects you probably need.
Let's study the basic anatomy of an effect, and then we'll look at some concrete examples.
import * as Fringe from "hyperapp-fringe-effects"
const fx = Fringe.someEffect({
action: SomeAction,
prop1: "foo",
prop2: "bar"
})
Fringe.someEffect is an effect constructor. It returns a tuple like the one below. The first element is the effect implementation, encapsulating the magic. The second element is your props object.
[
effect
{
action: SomeAction,
prop1: "foo",
prop2: "bar"
}
]
action is not a special property name, but you'll see it used very often as it's obvious what is for. This is the action you want Hyperapp to dispatch with the result of the effect. The other props are whatever data we need to pass to the effect. An HTTP effect will need at least a URL, while other effects may not even take properties.
How do we tell Hyperapp to run this effect? We do that through actions (#749).
const StartItAll = state => [
state,
Fringe.someEffect({
action: SomeAction,
prop1: "foo",
prop2: "bar"
})
]
This tells Hyperapp to update your state (in this case we're just pasing the same state object we received in the action) and run Fringe.someEffect.
Need to parallelize multiple effects?
const StartItAll = state => [
state,
Fringe.someEffect({
action: SomeAction,
prop1: "foo",
prop2: "bar"
}),
Fringe.anotherEffect({
action: AnotherAction
})
]
After Hyperapp is initialized, it will dispatch your init action to start your app, setting the state and running the Time.delay effect.
import { h, app } from "hyperapp"
import * as Time from "@hyperapp/time"
import { state, SendNotification } from "./app"
app({
init: [
state,
Time.delay({
action: SendNotification,
interval: 3 * Time.SECOND
})
]
// ...
})
Turns out you can use JSX to describe effects too.
import { h, app } from "hyperapp"
import * as Time from "@hyperapp/time"
import { state, SendNotification } from "./app"
app({
init: [state, <Time.delay action={SendNotification} interval={3 * SECOND} />]
// ...
})
import * as Http from "@hyperapp/http"
import { Populate } from "./actions"
const DownloadRepos = state => [
state,
Http.fetch({
url: "https://api.github.com/orgs/hyperapp/repos?per_page=100",
action: Populate
})
]
import * as Http from "@hyperapp/http"
import { Populate } from "./actions"
const DownloadRepos = state => [
state,
<Http.fetch
url={"https://api.github.com/orgs/hyperapp/repos?per_page=100"}
action={Populate}
/>
]
import { h, app } from "hyperapp"
import * as Random from "@hyperapp/random"
const NewFace = (state, newFace) => ({
...state,
dieFace: Math.floor(newFace)
})
const Roll = state => [
state,
Random.generate({ min: 1, max: 10, action: NewFace })
]
app({
init: Roll({ dieFace: 1 }),
view: state => (
<div>
<h1>{state.dieFace}</h1>
<button onClick={Roll}>Roll</button>
</div>
),
container: document.body
})
import { h, app } from "hyperapp"
import * as Random from "@hyperapp/random"
const NewFace = (state, newFace) => ({
...state,
dieFace: Math.floor(newFace)
})
const Roll = state => [
state,
<Random.generate min={1} max={10} action={NewFace} />
]
app({
init: Roll({ dieFace: 1 }),
view: state => (
<div>
<h1>{state.dieFace}</h1>
<button onClick={Roll}>Roll</button>
</div>
),
container: document.body
})
If you want to implement effects, one of these things are happening:
PhaserLink and there is no Hyperapp effect for it. Let's implement an effect adapter for setTimeout, there's not much to it. There are essentially two parts to implementing an effect:
An effect function. It encapsulates the implementation of the effect. It receives your props and the dispatch function so you can send messages back to Hyperapp.
An effect constructor (optional). A function that takes your props and returns a tuple with an effect function and props.
const effect = (dispatch, props) =>
setTimeout(() => dispatch(props.action), props.interval)
export const delay = props => [effect, props]
Why cache the effect function? If we create a new function from scratch every time we invoke an effect constructor, we wouldn't be able to assert deepEqual(effect1, effect2).
Consider an action returning an effect that generates random numbers. We don't need to compare any numbers to verify our effect works with Hyperapp. We'll test if the action returns the effect we want instead.
import { LuckyRoll, NewFace } from "./actions"
import * as Random from "@hyperapp/random"
import { deepEqual } from "assert"
const [, fx] = LuckyRoll({ luck: 0.9, cheat: false })
deepEqual(
fx,
Random.generate({
min: 1,
max: 10,
action: NewFace
})
)
Thanks for writing this up π
Glad I could ask the painful questions π
So when do we start making repo(s) for things like @hyperapp/time and @hyperapp/http?
I'm on board with making them separate packages, but does each deserve its own repo or would a monorepo be better for the FX packages?
I like your little mono repo tbh
As long as it's tree shake-able, I don't see the harm π
But some Effects might get gnarly and need to have their own too. I think most light wrappers can go to monoland and heavy wrappers might need to be their own thing
@selfup tree-shaking is a separate issue since I'm still on board with individual packages for each, allowing users to only add dependencies on the FX they want to use.
On a related note - I'm not sure how I feel about subscriptions, and if they deserve to be separate or not.
I am open to anything tbh, I just know that setting up each repo with the same rollup config will get tiring. From a user perspective it's great to have individual things, just worried about maintenance π
Yea subs are a weird one. Down to open the floor on this one for sure
@okwolf My current plan is to create a @hyperapp/gizmo repository for every _domain_, e.g., @hyperapp/time should include delay effects, tick subscriptions, constants, and (maybe) even utility functions.
@selfup Yeah, I know! Fortunately, the build configuration part of the process can be automated, the hard part is and has always been writing and keeping the documentation up to date.
@jorgebucaran The main advantage to using a monorepo would be making some of that automation easier - since the FX would always be in folders adjacent to each other so scripts could reliably add new or update existing FX libraries. This includes chores like updating a dependency version across all FX with one operation.
With that said, I'm OK with separate repos per package. The main advantages with that way would be dedicated issue tracking, contributing to some repos but not others, and tighter control over creating new FX.
@okwolf Noted. How do you manage your monorepos by the way?
@jorgebucaran How do you manage your monorepos by the way?
I don't have much experience in this area myself. But if you want to make a political statement you're apparently supposed to use Lerna.
Fascinating. I wouldn't use Lerna (or any other software) anyway. A simple script which I'll write myself should do _if_ I go the monorepo way.
@jorgebucaran JSX
Wouldn't those examples end up being wrapped in the h function after compilation? Seems like a strange thing to do.
Beyond that, I think it looks good :+1:
@SkaterDad since JSX compiles into calls to our h function, and that function directly returns the results of βcomponentsβ that are passed as the tag. This way of calling functions may not be intuitive to React users, since it always returns a wrapper object around components instead of directly using the value returned by your component like Hyperapp does.
This way of calling functions may not be intuitive to React users,
I'd guess it's going to be confusing to more than React users if JSX is promoted for working with Effects.
In the examples posted, in my view there isn't a clear benefit to using JSX.
I see how it works, but I can't imagine doing it in real code. In the view functions, it makes _some_ sense, since it's mimicking HTML. Even there, I've stopped using it in my own projects.
@SkaterDad - IMO I quite like the JSX style as it seems to better communicate the declarative nature of things. There's not really a logical reason I can attribute to this feeling except maybe I'm thinking back to other declarative languages that used XML. Just my 2 cents worth.
@SkaterDad We'll have to wait to see how it pans out. Since effects and subscriptions are never directly created inside the view, I find using JSX a breath of fresh air as it makes my application code feel less monotonous. The good news is that using JSX to create effects (or subscriptions) is entirely up to you!
I'm completely with @SkaterDad on this one - not only because using JSX in this way is confusing to React users.
It's confusing to everyone, because that's just not what JSX is for - not even in HyperApp itself, where this syntax makes it harder to distinguish virtual DOM trees from effect initializations.
What's even more confusing, is that passing this through the h() function literally doesn't do anything - so it unnecessarily raises the question "what does that do?", to which the answer is "nothing, it just happens to work for effects as well as DOM nodes".
Using JSX syntax also would appear to imply you can pass effects with children. (can you??)
Passing an object literal is the idiomatic way to do named arguments in JS - everyone is completely familiar with it, so using JSX here is just surprising and a little cheeky. (just because you can!)
You're also adding some unnecessary extra calls to the footprint of the compiled code.
You're also committing yourself to an h() function that always (going forward) will support effects. (but basically just ignore them. which is weird.)
Finally, I think you'll have quite the challenge trying to type-hint this mixed JSX use for Typescript.
I hope you don't encourage using JSX and the h() function in this manner - it's not what either of them were intended for, it's unnecessary, and it's confusing. I'm not sure what good it does ("more declarative" is just a feel, not a measurable quality) but it definitely does more harm than good.
@mindplay-dk Thanks for your input, but it doesn't add much to the discussion. Using JSX to create effects/subscriptions is entirely up to you! If you don't like it, don't use it. Period.
π If you have any worthwhile questions about the effects API feel free to comment again.
@jorgebucaran: I agree with @mindplay-dk and I think the comment was worthwhile. Why would you encourage JSX syntax other than preference? Is there _any_ benefit of using JSX over the Vanilla JS syntax? When I saw the JSX syntax my first reaction was "wait, isn't this an _effect_?".
I've given up fighting the case against JSX, so I withheld my opinion until now. I respect personal preference. But I do find it confusing in this case and what is more, I fear that the APIs are being defined (or rather valid variations being dismissed - like currying and positional arguments) so that this style can be maintained. Although, I realise there are other valid arguments against these variations it would be nice to know that the decisions were made impartially without bias toward JSX.
@lukejacksonn I fear that the APIs are being defined (or rather valid variations being dismissed - like currying and positional arguments) so that this style can be maintained.
No, named arguments are idiomatic and that's why I prefer them to positional arguments or currying arguments. Even if the proposed effects API used positional arguments it would be easy to write effects in JSX using a facade that translates their signature to what the JSX parser expects.
@ryanhaney Don't use JSX to write your effects or subscriptions. It's not mandatory. But let others who like it use it if they prefer it.
If we are worrying about future examples in the documentation using JSX, then let's not. I'll use plain JavaScript for all the examples. I'm happy just mentioning that JSX can be used too.
I have nothing against named arguments, they work great. Just wanted to voice my concerns around that issue. That is all I have to say on the matter, thanks for the confirmation.
If we are worrying about future examples in the documentation using JSX, then let's not.
βοΈ This I think, is quite important though. Glad to hear you consider it.
I'll use plain JavaScript for all the examples.
π
I have a effects related question regarding development against mock data. It is very convenient to e.g. avoid real https requests during development and work against mock, or in case of Random() to have control about what number is going to be generated when debugging the app.
Would be such thing possible with effects?
@mshgh Yeah, I think so! Since effects separate out everything that is a side effect, you could just import your own fake side effects instead of the real ones while in debug mode.
At least if you're using CommonJS modules you could:
var Random
if (process.env.NODE_ENV === 'development') {
Random = require('./mocks/fakeRandom')
else {
Random = require('@hyperapp/fx').Random
}
...
I'm not sure if the same is possible, or how with Es6 modules though π€
@mshgh One of the big reasons, if not _the_ reason for all these changes in Hyperapp is to simplify testing, so, yes, some of those things, will be possible with effects.
With Random() effects, what number will be generated is not important. Instead, we want to test if our action produces an effect that says it will generate a random number. There will be no actual random numbers generated. It's a strict equality check.
Similarly, with an Http() effect, we don't care what data the server will send, but whether our action produces an effect that says it will fetch some data from our server with some specific parameters. No actual HTTP request will be created.
Those will be only the tests that assess your effects (technically actions that produce effects).
Those will not be the only tests in your application. You probably want other tests as well. For example, a test that checks whether some UI behaves appropriately when you try to render some null or invalid data.
What @jorgebucaran says is true, but if I'm not mistaken I think @mshgh was not so much talking about testing as in unit testing, but rather about live-debugging an app with faked effects (to use a fake backend, with fixed data for example). At least that's the perspective I took in my response. Just to clarify :)
Unit-testing is a far more common type of test probably, but anyhow: both types of testing/debugging are equally more easy thanks to separate effects.
@zaceno is correct. I got the part effect itself is "just" data, so it is easy to test if the effect function generates what is expected. I was curious about the live-debugging with fake backend. As put above.
So I should implement my own fakeHttp effect. It would work for me I think. Thank you guys for your quick answers.
Thank you, @zaceno for your clarification and @mshgh for the question!
If anyone has additional questions about effects and how to use them, please create a separate issue, e.g., an issue about unit-testing actions and effects. ππ
The FX API rocks! π
P.s I wanted to make some enquiries.
In the case of running multiple effects at once as demonstrated here:
const StartItAll = state => [
state,
[
Fringe.someEffect({
action: SomeAction,
prop1: "foo",
prop2: "bar"
}),
Fringe.anotherEffect({
action: AnotherAction
})
]
]
I was wondering if the effects are run by the hyperapp runtime one before the other in order of the array indices.
This is because sometimes I only want an asynchronous task to run if the previous async task resolves successfully and then use whatever data gotten from there.
I'm really appreciative of the FX API and I'm adapting to it, however this issue is trivial to me.
@ThobyV In your example every effect will run concurrently. To do what you are asking for, someEffect's action should return anotherEffect. That way, anotherEffect will run only after someEffect is finished.
It should be possible to come up with a library of effect utilities to spice this up.
Thanks for the clarification @Jorgebucaran. I think I understand that well.
I'll really look forward to the FX utility library.
The Http FX library won't be able to run google's firebase abstracted API as it uses websockets/fetch under the hood without exposing a URL.
It also happens that most times I need to run a chain of abstracted queries.
I'm building an application using HAV2 and might have to write the firebase effects myself.
Why not just provide a callback to the component to update it self with its own local state?
https://gist.github.com/k1r0s/be41597d9dbe3a8a4ebcaa1131f6a29b
Therefore you can perform http calls, effects, etc
@k1r0s What about multiple stateful components?
Hi @k1r0s! This question sounds like: why not just do it like do in V1? This is a good question, but it doesn't belong in this issue. If you want to discuss the goals of V2 see https://github.com/jorgebucaran/hyperapp/issues/672, but keep in mind that issue is a bit outdated now.
Just FYI that this is virtually posible even on superfine
Yes, and it should. Superfine is great if you don't care about Hyperapp's built-in state container or want to manage your state using one of the various third-party state containers out there, e.g. Redux, Mobx.
@k1r0s What about multiple stateful components?
I guess that thingy could be accomplish through a HOC
Most helpful comment
I'd guess it's going to be confusing to more than React users if JSX is promoted for working with Effects.
In the examples posted, in my view there isn't a clear benefit to using JSX.
I see how it works, but I can't imagine doing it in real code. In the view functions, it makes _some_ sense, since it's mimicking HTML. Even there, I've stopped using it in my own projects.