agnostic/effect
In many frontend 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.)
Effect timing
Effects created with from carry no timing guarantee beyond
running during effect processing of the dispatch that produced them.
Deferred effects — those tied to a point in a platform’s render lifecycle —
are provided by the platform modules instead of this one:
dom.before_paint and
dom.after_paint for the browser DOM,
and opentui/effect.after_flush
for OpenTUI. Platform authors can build their own deferred effects with
deferred.
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(message)
Values
pub fn batch(effects: List(Effect(message))) -> Effect(message)
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
messagetype 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 deferred(
phase phase: String,
effect effect: fn(fn(message) -> Nil, dynamic.Dynamic) -> Nil,
) -> Effect(message)
Construct an effect deferred to a platform-declared phase. This is the
platform-author API: platform modules wrap it with typed constructors
(e.g. dom.before_paint or
opentui/effect.after_flush),
pairing each constructor with a Phase of the
same name in that platform’s phases declaration. Application code should
prefer those typed constructors.
In addition to the dispatch function, your callback will also be provided
with the root of your app or component as provided by the platform.
Note: if the running platform does not declare
phase, the effect is silently dropped and never run. This is what happens to all deferred effects in server components, which declare no phases.
pub fn from(
effect: fn(fn(message) -> Nil) -> Nil,
) -> Effect(message)
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 agnostic/effect.{type Effect}
import plinth/browser/window
type Model {
Model(Int)
}
type message {
FetchState
}
fn init(_flags) -> #(Model, Effect(message)) {
#(
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!
pub fn none() -> Effect(message)
Most Lustre applications need to return a tuple of #(model, Effect(message))
from their init and update functions. If you don’t want to perform any
side effects, you can use none to tell the runtime there’s no work to do.
pub fn provide(key: String, value: json.Json) -> Effect(message)
Provide a context value to child components in the DOM that this Lustre app
didn’t render. This occurs in components with that render one or more <slot>
elements in their view function.
Once a value for the given key has been provided, children can subscribe
to changes and receive updates any subsequent times provide is called with
the same key. This facilitates parent-child communication even in cases where
the parent doesn’t own the child element directly.
Note: This is one half of the WCCG Context Protocol
and will work in tandem with not just Lustre components but any third-party
Web Component that implements the context-request event.
pub fn subscribe(
key: String,
decoder: decode.Decoder(message),
) -> Effect(message)
Subscribe to changes for a context value provided by a parent element in the DOM. This effect will decode the context value from the first parent element that has already provided a context for this key at least once. Once a subscription is set up, any changes to the context value will trigger additional messages to be dispatched with the new decoded value.
If no parent elements have provided a context for the given key at the time this effect is run, no subscription is set up even if a parent later provides a context for this key.
Note: Pay attention to timing and lifecycle differences between applications and components. Components that need to subscribe to a context should make sure this effect is called after the component has connected.
Note: This is one half of the WCCG Context Protocol and will work in tandem with not just Lustre components and applications, but any third-party Web Component that acts as a context provider.
pub fn unsubscribe(key: String) -> Effect(message)
Unsubscribe from a context subscription that was previously
set up for this key.