The effect
hook is a way to perform side effects such as synchronizing with an external system.
The effect is executed when the component is first mounted and whenever any of the
dependencies specified change and can optionally specify a cleanup function.
Let's take a look at an example of a clock component that uses an effect hook to start a timer and update the time every second.
import gleam/io
import gleam/erlang
import gleam/option.{type Option, None, Some}
import sprocket/context.{type Context, dep}
import sprocket/component.{render}
import sprocket/hooks.{effect, reducer}
import sprocket/html/elements.{fragment, span, text}
import sprocket/internal/utils/timer.{interval}
type Model {
Model(time: Int, timezone: String)
}
type Msg {
UpdateTime(Int)
}
fn update(model: Model, msg: Msg) -> Model {
case msg {
UpdateTime(time) -> {
Model(..model, time: time)
}
}
}
fn initial() -> Model {
Model(time: erlang.system_time(erlang.Second), timezone: "UTC")
}
pub type ClockProps {
ClockProps(label: Option(String), time_unit: Option(erlang.TimeUnit))
}
pub fn clock(ctx: Context, props: ClockProps) {
let ClockProps(label, time_unit) = props
// Define a reducer to handle events and update the state
use ctx, Model(time: time, ..), dispatch <- reducer(ctx, initial(), update)
// Example effect with an empty list of dependencies, runs once on mount
use ctx <- effect(
ctx,
fn() {
io.println("Clock component mounted!")
None
},
[],
)
let time_unit =
time_unit
|> option.unwrap(erlang.Second)
// Example effect that has a cleanup function and runs whenever `time` or `time_unit` changes
use ctx <- effect(
ctx,
fn() {
let interval_duration = case time_unit {
erlang.Millisecond -> 1
_ -> 1000
}
let update_time = fn() {
dispatch(UpdateTime(erlang.system_time(time_unit)))
}
let cancel = interval(interval_duration, update_time)
Some(fn() { cancel() })
},
[dep(time), dep(time_unit)],
)
let current_time = format_utc_timestamp(time, time_unit)
render(ctx, case label {
Some(label) ->
fragment([span([], [text(label)]), span([], [text(current_time)])])
None -> fragment([text(current_time)])
})
}
@external(erlang, "print_time", "format_utc_timestamp")
pub fn format_utc_timestamp(time: ErlangTimestamp, unit: erlang.TimeUnit) -> String
We can also use a bit of erlang code here to handle formatting the timestamp, which demonstrates
how simple it is to call some erlang functionality from Gleam using FFI. Using the @external
attribute, we can declare typed erlang interfaces in our Gleam code. In this case, we're calling the
format_utc_timestamp
function defined in our print_time
module. This function takes an erlang
timestamp and a time unit and returns a formatted string. The erlang code for this module is shown
below.
-module(print_time).
-export([format_utc_timestamp/2]).
format_utc_timestamp(Timestamp, Unit) ->
{_, _, Micro} = Timestamp,
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_universal_time(Timestamp),
Mstr = element(
Month, {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
),
Meridiem =
case Hour < 12 of
true -> "AM";
false -> "PM"
end,
MeridiemHour =
case Hour > 12 of
true -> Hour - 12;
false -> Hour
end,
Formatted =
case Unit of
millisecond ->
Milli = Micro div 1000,
io_lib:format("~2w ~s ~4w ~2w:~2..0w:~2..0w.~3..0w ~s", [
Day, Mstr, Year, MeridiemHour, Minute, Second, Milli, Meridiem
]);
_ ->
io_lib:format("~2w ~s ~4w ~2w:~2..0w:~2..0w ~s", [
Day, Mstr, Year, MeridiemHour, Minute, Second, Meridiem
])
end,
erlang:iolist_to_binary(Formatted).
In this example, we have a clock component that uses an effect hook to start a timer and update the
time every second. The effect hook is defined with a dependency on the time_unit
prop. This means
that the effect will run on the initial mount and whenever the time unit changes. The effect hook
returns a cleanup function that cancels the timer. This cleanup function is called when the
component is unmounted or when the effect hook is re-run. This is a great way to ensure that
resources are cleaned up when the component is unmounted and they are no longer needed.
This example also prints a message to the console when the component is mounted using an effect hook with an empty list of dependencies. This effect will only run once when the component is first mounted.
We can also render SVG to create an analog clock component.
There may be situations where you need to perform side effects in your components on every render,
in which case you can use a single-item list of dependencies that includes the ctx
variable
[dep(ctx)]
. This will cause the effect to run on every render, since the ctx
variable will
always be different on each render.