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 withmessage
, 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 agetRandomGif
iffetchURL
succeeds.LoadFailure
- Fed back by agetRandomGif
iffetuchURL
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 theupdate
.
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.