In this example we show the basic structure of all Lustre applications with a classic counter example.
All Lustre applications are built around the Model-View-Update (MVU) architecture. This is a pattern that's been popularised by the Elm programming language and has since been adopted by many other frameworks and languages.
MVU applications are built around three main concepts:
- A
Model
and a function to initialise it. - A
Msg
type and a function to update the model based on messages. - A
View
function to render the model as a LustreElement
.
These three pieces come together to form a self-contained update loop. You produce an initial model, render it as HTML, and convert any user interactions into messages to handle in the update function.
+--------+
| |
| update |
| |
+--------+
^ |
| |
Msg | | Model
| |
| v
+------+ +------------------------+
| | Model | |
| init |------------------------>| Lustre Runtime |
| | | |
+------+ +------------------------+
^ |
| |
Msg | | Model
| |
| v
+--------+
| |
| view |
| |
+--------+
The model represents the entire state of your application. For most Lustre
applications this will be a record, but for this example we're aliasing Int
to
our Model
type to keep things simple.
We also need to write an init
function that returns the initial state of our
application. It takes one argument, known as "flags" which is provided when the
application is first started.
fn init(initial_count: Int) -> Model {
case initial_count < 0 {
True -> 0
False -> initial_count
}
}
Our init
function takes a starting count, but ensures it cannot be below 0
.
In many other frameworks, it's common to update state directly in an event handler.
MVU applications take a different approach: instead of state updates being scattered
around your codebase, they are handled in a single update
function.
To achieve this, we define a Msg
type that represents all the different kinds of
messages our application can receive. If you're familiar with Erlang this approach
to state management will be familiar to you. If you're coming from a JavaScript
background, this approach is most-similar to state management solutions like Redux
or Vuex.
pub opaque type Msg {
Incr
Decr
}
This approach means it is easy to quickly get an idea of all the ways your app
can change state, and makes it easy to add new state changes over time. By pattern
matching on an incoming message in our update
function, we can lean on Gleam's
exhaustiveness checking to ensure we handle all possible messages.
Because state management is handled in our update
function, our view
becomes
a simple function that takes a model and returns some HTML in the form of a
Lustre Element
.
fn view(model: Model) -> Element(Msg) {
...
}
In Lustre we call all functions that return an Element
"view functions": there's
nothing special about the view
that takes your model.
Folks coming from frameworks like React might notice the absence of components with local encapsulated state. Lustre does have components like this, but unlike other frameworks these are a fairly advanced use of the library and are typically used for larger pieces of UI like an entire form or a table. We'll cover how components fit into Lustre in later examples, but for now resist the urge to think in terms of "components" and "state" and try to think of your UI as a composition of view functions.
In the previous example we used the lustre.element
function to construct a
static Lustre app. To introduce the basic MVU loop, we can use lustre.simple
instead. From now on we'll see that all the different ways to construct a Lustre
application all take the same three init
, update
, and view
functions.
Starting a Lustre application with lustre.start
requires three things:
-
A configured
Application
(that's what we usedlustre.element
for). -
A CSS selector to locate the DOM node to mount the application on to. As in other frameworks, it's common to use an element with the id "app": for that you'd write the selector as
#app
. -
Some initial data to pass to the application's
init
function. Because applications constructed withlustre.element
are not dynamic there's nothing meaningful to pass in here, so we just useNil
.
Starting an application could fail for a number of reasons, so this function
returns a Result
. The Ok
value is a function you can use to send messages to
your running application from the outside world: we'll see more of that in later
examples!
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.