lustre/effect
In other frameworks it’s common for components to perform side effects whenever the need them. An event handler might make an HTTP request, or a component might reach into the DOM to focus an input.
In Lustre we try to keep side effects separate from our main program loop. This comes with a whole bunch of benefits like making it easier to test and reason about our code, making it possible to implement time-travel debugging, or even to run our app on the server using Lustre’s server components. This is great but we still need to perform side effects at some point, so how do we do that?
The answer is through the Effect
type that treats side effects as data.
This approach is known as having managed effects: you pass data that
describes a side effect to Lustre’s runtime and it takes care of performing
that effect and potentially sending messages back to your program for you.
By going through this abstraction we discourage side effects from being
performed in the middle of our program.
Related packages
While Lustre doesn’t include many built-in effects, there are a number of community packages define useful common effects for your applications.
-
rsvp
– Send HTTP requests from Lustre applications and server components. -
modem
– A friendly Lustre package to help you build a router, handle links, and manage URLs. -
plinth
– Bindings to Node.js and browser platform APIs. (This package does not include any effects directly, but it does provide bindings to many APIs that you can use to create your own.)
Examples
For folks coming from other languages (or other Gleam code!) where side effects are often performed in-place, this can feel a bit strange. We have a category of example apps dedicated to showing various effects in action:
This list of examples is likely to grow over time, so be sure to check back every now and then to see what’s new!
Getting help
If you’re having trouble with Lustre or not sure what the right way to do something is, the best place to get help is the Gleam Discord server. You could also open an issue on the Lustre GitHub repository.
Types
The Effect
type treats side effects as data and is a way of saying “Hey
Lustre, do this thing for me.” Each effect specifies two things:
-
The side effects for the runtime to perform.
-
The type of messages that (might) be sent back to the program in response.
pub opaque type Effect(msg)
Values
pub fn after_paint(
effect: fn(fn(a) -> Nil, Dynamic) -> Nil,
) -> Effect(a)
Schedule a side effect that is guaranteed to run after the browser has painted the screen.
In addition to the dispatch
function, your callback will also be provided
with root element of your app or component. This is especially useful inside
of components, giving you a reference to the Shadow Root.
Note: There is no concept of a “paint” for server components. These effects will be ignored in those contexts and never run.
pub fn batch(effects: List(Effect(a))) -> Effect(a)
Batch multiple effects to be performed at the same time.
Note: The runtime makes no guarantees about the order on which effects are performed! If you need to chain or sequence effects together, you have two broad options:
Create variants of your
msg
type to represent each step in the sequence and fire off the next effect in response to the previous one.If you’re defining effects yourself, consider whether or not you can handle the sequencing inside the effect itself.
pub fn before_paint(
effect: fn(fn(a) -> Nil, Dynamic) -> Nil,
) -> Effect(a)
Schedule a side effect that is guaranteed to run after your view
function
is called and the DOM has been updated, but before the browser has
painted the screen. This effect is useful when you need to read from the DOM
or perform other operations that might affect the layout of your application.
In addition to the dispatch
function, your callback will also be provided
with root element of your app or component. This is especially useful inside
of components, giving you a reference to the Shadow Root.
Messages dispatched immediately in this effect will trigger a second re-render of your application before the browser paints the screen. This let’s you read the state of the DOM, update your model, and then render a second time with the additional information.
Note: dispatching messages synchronously in this effect can lead to degraded performance if not used correctly. In the worst case you can lock up the browser and prevent it from painting the screen at all.
Note: There is no concept of a “paint” for server components. These effects will be ignored in those contexts and never run.
pub fn from(effect: fn(fn(a) -> Nil) -> Nil) -> Effect(a)
Construct your own reusable effect from a custom callback. This callback is
called with a dispatch
function you can use to send messages back to your
application’s update
function.
Example using the window
module from the plinth
library to dispatch a
message on the browser window object’s "visibilitychange"
event.
import lustre/effect.{type Effect}
import plinth/browser/window
type Model {
Model(Int)
}
type Msg {
FetchState
}
fn init(_flags) -> #(Model, Effect(Msg)) {
#(
Model(0),
effect.from(fn(dispatch) {
window.add_event_listener("visibilitychange", fn(_event) {
dispatch(FetchState)
})
}),
)
}
pub fn map(effect: Effect(a), f: fn(a) -> b) -> Effect(b)
Transform the result of an effect. This is useful for mapping over effects produced by other libraries or modules.
Note: Remember that effects are not required to dispatch any messages. Your mapping function may never be called!