Lustre for Elm developers

Lustre has been directly inspired by Elm and shares some of the primary architectural features of “The Elm Architecture”, otherwise known as Model-View-Update. This guide is for Elm developers who are new to Lustreand want to get up to speed quickly.

How do I…?

Setup a new project

In Elm, all you really need to get started is to install the elm binary. Running elm make against an Elm file will transpile your code to either Javascript, or HTML with the Javascript output inlined. Most people will build out their own toolchain to support build-on-save and hot-reload, with tools like Vite or Webpack with the appropriate plugins. A simple hello world might look like this:

module Main exposing (main)

import Html

main =
    Html.text "Hello, world"

In Lustre you need to install the lustre package with gleam add lustre. Most Lustre projects will add the dev tools too with gleam add --dev lustre_dev_tools. A simple hello world might look like this:

import lustre
import lustre/element/html

pub fn main() {
  let app = lustre.element(html.h1([], [html.text("Hello, world")]))
  let assert Ok(_) = lustre.start(app, "#app", Nil)
}

Render some HTML

In Elm, you can call functions in the elm/html package to render HTML elements. The Html module in elm/html contains functions for most standard HTML tags; these functions take as parameters a list of attributes from Html.Attributes, or events from Html.Events - as well as a list of child elements:

Html.button
  [ Html.Attributes.class "primary"
  , Html.Events.onClick ButtonClicked
  ]
  [ Html.text "Click me" ]

In Lustre, HTML is rendered by calling functions, many of whom share the same signature - functions in lustre/element/html represent HTML tags, and most functions accept a list of lustre/attribute or lustre/event values, as well as a list of child elements.

html.button([attribute.class("primary"), event.on_click(ButtonClicked)], [
  html.text("Click me")
])

Render some text

In Elm, text is rendered by passing a String to the Html.text function:

Html.span [] [ Html.text <| "Hello, " ++ name ]

In Lustre, text is rendered by passing a String to the html.text or element.text functions:

html.span([], [
  html.text("Hello, " <> name),
])

Manage state

In Elm all state is stored in a single Model type and updates happen through a central update function:

type alias Model = Int

init : Model
init = 0

type Msg
    = Incr
    | Decr

update : Msg -> Model -> Model
update msg model =
    case msg of
        Incr ->
            model + 1

        Decr ->
            model - 1

In Lustre all state is stored in a single Model type and updates happen through a central update function, just like in Elm:

fn init(_) {
  0
}

type Msg {
  Incr
  Decr
}

fn update(model: Model, msg: Msg) -> Msg {
  case msg {
    Incr -> model + 1
    Decr -> model - 1
  }
}

Handle events

In Elm event handlers are decoders for event objects. When the decoder succeeds, that value is dispatched as a message to your update function.

Html.input [ Html.Events.onInput UserUpdatedNameField ] []

type Msg
  = UserUpdatedNameField String

type alias Model = { name : String }

update : Msg -> Model -> Model
update msg model =
  case msg of
    UserUpdatedNameField name
      { model | name = name }

In Lustre event handlers work in the same way. Lustre provides functions to handle most common events, in lustre/effect:

button([on_click(Decr)], [text("-")])
input([on_input(UpdateInput)])
div([on("mousemove", fn(event) {
  ...
}], [...])

Fetch data

In Elm you can fetch data by making a HTTP request. HTTP request functions both return a value of type Cmd msg, and are handled by the application’s update function; the payload from the response is available within the update function and can be used to update the Model, or call other functions that return a value of type Cmd msg.

type Msg
  = ApiReturnedBookResponse (Result Http.Error String)

getBook : Cmd Msg
getBook =
  Http.get
    { url = "https://elm-lang.org/assets/public-opinion.txt"
    , expect = Http.expectString ApiReturnedBookResponse
    }

type alias Model = { bookResponse : Result Http.Error String }

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    ApiReturnedBookResponse response ->
      { model | bookResponse = response }

In Lustre, the approach is similar, using types and functions from the lustre_http package:

pub type Msg {
  ApiReturnedBookResponse(Result(String, lustre_http.HttpError))
}

fn get_book() -> effect.Effect(Msg) {
  lustre_http.get(
    "https://elm-lang.org/assets/public-opinion.txt",
    lustre_http.expect_text(ApiReturnedBookResponse)
  )
}

pub type Model {
  Model(book_response: Result(String, HttpError))
}


pub fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) {
  case msg {
    ApiReturnedBookResponse(response) -> #(Model(..model, book_response: response), effect.none())
  }
}

Where to go next

To walk through setting up a new Lustre project and building your first app, check out the quickstart guide.

If you prefer to learn by example, we have a collection of examples that show off specific features and patterns in Lustre. You can find them in the examples directory

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.

Search Document