Effects

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).
6 Nov 2024 0:55:32 AM

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.