Effects

In reflex all effects are managed. We will dive into what it actually means little later. This features make it stand out from the popular alternatives like React + Redux.

Traditionally all the IO (Networking, Database Access, Filesystem access, Time Access, etc.) in JS is performed as side effects. That makes code non-deterministic. In plain English - Reproducing failures & writing tests for such code is very difficult. Traditionally mocking is used to tests such programs.

Richard Feldman a.k.a @rtfeldman gave an excellent talk Effects as Data about tradditional difficulties problems associated with side effects & how managed effects solve those.

Managed effects tackle non-determinism same as Redux tackles state updates - via centralized transactions. This makes traditionally difficult problems trivial.

Tasks

Tasks in reflex are the way to describe non-deterministic operations. They resemble functions in that they just describe an operation(s) & running them is a separate problem.

export const fetchURI =
  (url:string):Task<XMLHttpRequest, XMLHttpRequest> =>
  new Task((succeed, fail) => {
    const request = new XMLHttpRequest()
    request.open('GET', url, true)
    request.onload = () => succeed(request)
    request.onerror = () => fail(request)
    request.send()
  })

Tasks also deliberately resemble promises in that they allow chaining multiple steps. Although API is, again deliberately, slightly different to avoid confusion.

fetchURL(myURL)
  // Perform other task if first on failed.
  .capture(failedRequest => fetchURI(myOtherURL))
  // Map respones if above task succeeded 
  .map({responseText}) => ({ status: "Ok", responseText })
  // Recover from the task failure by succeeding with value.
  .recover(({statusCode}) => ({ status: "Fail", statusCode })
  // Perform another task on succees.
  .chain(result => new Task(...))

Note: There are different chaining methods focused on specific operation instead of uniform then. This better communicates intent to a reader and provides better context to a type checker to work with.

Effects

In reflex Effect is a description of result of the Task instance is fed back into update. It is also how you schedule a task to perform it's operations.

export type Message =
  | { type: "ReceivedNewGif", url: string }
  | { type: "LoadFailure", code: number }
  // ...

const getRandomGif =
  (topic:string):Effect<Message> =>
  Effects.perform
  ( fetchURI(makeRandomURI(topic))
    .map(decodeResponse)
    .recover(decodeFailure)
  )

const decodeResponse = ({responseText}) => ({
  type: "ReceivedNewGif",
  url: JSON.parse(responseText).data.image_url
})

const decodeFailure = ({statusText}) => ({
  type: "LoadFailure",
  code: statusText
})

In the example above getRandomGif function maps result of fetchURI(...) task to ReceivedNewGif message if it succeeds or to LoadFailure message if it fails. Important detail is that Effects.perform is always passed task that succeeds with a Message. Result is an Effect that feeds back Message into update.

Note: Unlike in promises it is impossible to not handle an error case. Effect<message> can only be created from task that always succeeds with message, type checker reports an error otherwise.

Example

In this example we will define component that fetches a random GIF when user asks for another image & display it.

As always, we will start out by guessing at what your Model should be:

// Model

export class Model {
  topic: string;
  url: ?string;
  constructor(topic:string, url:string) {
    this.topic = topic
    this.url = url
  }
}

In this case let's first sketch out the view function because it will give a better idea what our example is gonig to look like.

export const view =
  (model:Model, address:Address<Message>):DOM =>
  html.main({
    style: {
      width: "200px",
      textAlign: "center"
    }
  }, [
    html.h2({
    }, [model.topic]),
    html.div({
      style: {
        display: "inline-block",
        width: "200px",
        height: "200px",
        lineHeight: "200px",
        backgroundPosition: "center center",
        backgroundSize: "cover",
        backgroundImage: `url("${model.uri}")`
      }
    }, [model.url == null ? "Loading..." : ""]),
    html.button({
      onClick: forward(address, createRequestMore)
    }, ["More Please!"])
  ])

 const createRequestMore = () => ({ type: "RequestMore" })

So this is typical. Same stuff as in basic examples. When you click our <button> it is going to produce a RequestMore message, so let's move on to defining our Message type.

export type Message =
  | { type: "RequestMore" }
  | { type: "ReceivedNewGif", url: string }
  | { type: "LoadFailure", code: number }

As you make have noticed Message type here is same as one defined in Effects section along with getRandomGif function.

So basically our component supports three messages:

  • RequestMore - Received when user clicks <button>
  • ReceivedNewGif - Fed back by a getRandomGif if fetchURL succeeds.
  • LoadFailure - Fed back by a getRandomGif if fetuchURL fails.

Moving on to an update function that will handle those messages:

export const update =
  (model:Model, message:Message):[Model, Effects<Message>] => {
    switch (message.type) {
      case "ReceivedNewGif":
        return [
          new Model(model.topic, message.url),
          Effects.none
        ]
      case "RequestMore":
        return [
          model,
          getRandomGif(model.topic)
        ]
      case "LoadFail":
        // TODO: Actually handle error
        console.error("LoadFail")
        return [
          model,
          Effects.none
        ]
    }
  }

Now the update function has the same overall shape as in basic examples, but the return type is a bit different. Instead of just giving back a Model, it produces both a Model and an Effect. The idea is: we still want to step the model forward, but we also want to do some stuff. In our case, we want to run a task that will fetch us random GIF url for the given topic, or do nothing hence - Effects.none

Note: Unlike in React + Redux tasks are scheduled as a part of update. This is subtle, but important detail, because in this case task depends on topic that is part of state that can also change through the update.

Finally, lets create an init function to get everything started:

export const init =
  (topic:string="Funny Cats"):[Model, Effects<Message>] =>
  [ new Model(topic, null)
  , getRandomGif(topic)
  ]

Function init changed from basic examples the way update did. In this case it returns Model and an Effect to get URL for initial random GIF, that is so that user can see first GIF without clicking a button.

How is this better ?

For one this provides well defined rules on when and how to perform IO rather than going about it willy-nilly. More importantly reflex is now performing all that IO at its own schedule that can be optimized in different ways.

This also enables deterministic session record / reply tools. Since all of the non-deterministic operations are expressed with effects at reply reflex can simply not run associated tasks or complete them with former results.

Note This is already possible with React + Redux to some lesser degree.

// TODO: animations case

results matching ""

    No results matching ""