Skip to content
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 Effects #750

Closed
jorgebucaran opened this issue Aug 28, 2018 · 39 comments
Closed

V2 Effects #750

jorgebucaran opened this issue Aug 28, 2018 · 39 comments
Labels
discussion docs You read it here first
Milestone

Comments

@jorgebucaran
Copy link
Owner

jorgebucaran commented Aug 28, 2018

Summary

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.

Background

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.

Declarative effects

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
  })
]

Examples

Time delays

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
    })
  ]
  // ...
})

JSX

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} />]
  // ...
})

HTTP requests

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
  })
]

JSX

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}
  />
]

Generating random numbers

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
})

JSX

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
})

Implementing your own effects

If you want to implement effects, one of these things are happening:

  1. Hyperapp's core effect library lacks an critical effect. (Request it!)
  2. You've created a new service PhaserLink and there is no Hyperapp effect for it.
  3. You want to write your own effect. Period.

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
  })
)
@okwolf okwolf added the docs You read it here first label Aug 29, 2018
@selfup
Copy link
Contributor

selfup commented Aug 30, 2018

Thanks for writing this up 🎉

Glad I could ask the painful questions 😂

@okwolf
Copy link
Contributor

okwolf commented Aug 30, 2018

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?

@selfup
Copy link
Contributor

selfup commented Aug 30, 2018

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

@okwolf
Copy link
Contributor

okwolf commented Aug 30, 2018

@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.

@selfup
Copy link
Contributor

selfup commented Aug 30, 2018

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

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Aug 30, 2018

@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.

@jorgebucaran
Copy link
Owner Author

@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.

@okwolf
Copy link
Contributor

okwolf commented Aug 30, 2018

@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.

@jorgebucaran
Copy link
Owner Author

@okwolf Noted. How do you manage your monorepos by the way?

@okwolf
Copy link
Contributor

okwolf commented Aug 30, 2018

@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.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Aug 30, 2018

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.

@SkaterDad
Copy link
Contributor

@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 👍

@okwolf
Copy link
Contributor

okwolf commented Aug 30, 2018

@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.

@SkaterDad
Copy link
Contributor

SkaterDad commented Aug 30, 2018

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.

This was referenced Aug 31, 2018
@jdh-invicara
Copy link

jdh-invicara commented Aug 31, 2018

@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.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Aug 31, 2018

@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!

@mindplay-dk

This comment has been minimized.

@jorgebucaran

This comment has been minimized.

This was referenced Sep 1, 2018
@prodrammer
Copy link

@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?".

@lukejacksonn
Copy link
Contributor

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.

@jorgebucaran
Copy link
Owner Author

@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.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Sep 27, 2018

@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.

@lukejacksonn
Copy link
Contributor

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.

@mindplay-dk
Copy link

I'll use plain JavaScript for all the examples.

👍

@mshgh
Copy link

mshgh commented Oct 1, 2018

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?

@zaceno
Copy link
Contributor

zaceno commented Oct 1, 2018

@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 🤔

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Oct 1, 2018

@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.

@zaceno
Copy link
Contributor

zaceno commented Oct 1, 2018

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.

@mshgh
Copy link

mshgh commented Oct 1, 2018

@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.

@jorgebucaran
Copy link
Owner Author

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. 👋😄

@jorgebucaran jorgebucaran added this to the V2 milestone Oct 3, 2018
@ThobyV
Copy link

ThobyV commented Nov 23, 2018

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.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Nov 23, 2018

@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.

@ThobyV
Copy link

ThobyV commented Nov 23, 2018

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.

@k1r0s

This comment has been minimized.

@infinnie

This comment has been minimized.

@jorgebucaran

This comment has been minimized.

@k1r0s

This comment has been minimized.

@jorgebucaran

This comment has been minimized.

@k1r0s

This comment has been minimized.

@jorgebucaran jorgebucaran pinned this issue Dec 16, 2018
@jorgebucaran jorgebucaran changed the title V2 Effects API V2 Effects Mar 6, 2019
Repository owner locked as resolved and limited conversation to collaborators May 3, 2019
@jorgebucaran jorgebucaran unpinned this issue Apr 3, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
discussion docs You read it here first
Projects
None yet
Development

No branches or pull requests